Commit b28a10ae authored by Aleksa Sarai's avatar Aleksa Sarai Committed by Al Viro

selftests: add openat2(2) selftests

Test all of the various openat2(2) flags. A small stress-test of a
symlink-rename attack is included to show that the protections against
".."-based attacks are sufficient.

The main things these self-tests are enforcing are:

  * The struct+usize ABI for openat2(2) and copy_struct_from_user() to
    ensure that upgrades will be handled gracefully (in addition,
    ensuring that misaligned structures are also handled correctly).

  * The -EINVAL checks for openat2(2) are all correctly handled to avoid
    userspace passing unknown or conflicting flag sets (most
    importantly, ensuring that invalid flag combinations are checked).

  * All of the RESOLVE_* semantics (including errno values) are
    correctly handled with various combinations of paths and flags.

  * RESOLVE_IN_ROOT correctly protects against the symlink rename(2)
    attack that has been responsible for several CVEs (and likely will
    be responsible for several more).

Cc: Shuah Khan <shuah@kernel.org>
Signed-off-by: default avatarAleksa Sarai <cyphar@cyphar.com>
Signed-off-by: default avatarAl Viro <viro@zeniv.linux.org.uk>
parent fddb5d43
......@@ -40,6 +40,7 @@ TARGETS += powerpc
TARGETS += proc
TARGETS += pstore
TARGETS += ptrace
TARGETS += openat2
TARGETS += rseq
TARGETS += rtc
TARGETS += seccomp
......
# SPDX-License-Identifier: GPL-2.0-or-later
CFLAGS += -Wall -O2 -g -fsanitize=address -fsanitize=undefined
TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test
include ../lib.mk
$(TEST_GEN_PROGS): helpers.c
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <string.h>
#include <syscall.h>
#include <limits.h>
#include "helpers.h"
bool needs_openat2(const struct open_how *how)
{
return how->resolve != 0;
}
int raw_openat2(int dfd, const char *path, void *how, size_t size)
{
int ret = syscall(__NR_openat2, dfd, path, how, size);
return ret >= 0 ? ret : -errno;
}
int sys_openat2(int dfd, const char *path, struct open_how *how)
{
return raw_openat2(dfd, path, how, sizeof(*how));
}
int sys_openat(int dfd, const char *path, struct open_how *how)
{
int ret = openat(dfd, path, how->flags, how->mode);
return ret >= 0 ? ret : -errno;
}
int sys_renameat2(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath, unsigned int flags)
{
int ret = syscall(__NR_renameat2, olddirfd, oldpath,
newdirfd, newpath, flags);
return ret >= 0 ? ret : -errno;
}
int touchat(int dfd, const char *path)
{
int fd = openat(dfd, path, O_CREAT);
if (fd >= 0)
close(fd);
return fd;
}
char *fdreadlink(int fd)
{
char *target, *tmp;
E_asprintf(&tmp, "/proc/self/fd/%d", fd);
target = malloc(PATH_MAX);
if (!target)
ksft_exit_fail_msg("fdreadlink: malloc failed\n");
memset(target, 0, PATH_MAX);
E_readlink(tmp, target, PATH_MAX);
free(tmp);
return target;
}
bool fdequal(int fd, int dfd, const char *path)
{
char *fdpath, *dfdpath, *other;
bool cmp;
fdpath = fdreadlink(fd);
dfdpath = fdreadlink(dfd);
if (!path)
E_asprintf(&other, "%s", dfdpath);
else if (*path == '/')
E_asprintf(&other, "%s", path);
else
E_asprintf(&other, "%s/%s", dfdpath, path);
cmp = !strcmp(fdpath, other);
free(fdpath);
free(dfdpath);
free(other);
return cmp;
}
bool openat2_supported = false;
void __attribute__((constructor)) init(void)
{
struct open_how how = {};
int fd;
BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_VER0);
/* Check openat2(2) support. */
fd = sys_openat2(AT_FDCWD, ".", &how);
openat2_supported = (fd >= 0);
if (fd >= 0)
close(fd);
}
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#ifndef __RESOLVEAT_H__
#define __RESOLVEAT_H__
#define _GNU_SOURCE
#include <stdint.h>
#include <errno.h>
#include <linux/types.h>
#include "../kselftest.h"
#define ARRAY_LEN(X) (sizeof (X) / sizeof (*(X)))
#define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); })))
#ifndef SYS_openat2
#ifndef __NR_openat2
#define __NR_openat2 437
#endif /* __NR_openat2 */
#define SYS_openat2 __NR_openat2
#endif /* SYS_openat2 */
/*
* Arguments for how openat2(2) should open the target path. If @resolve is
* zero, then openat2(2) operates very similarly to openat(2).
*
* However, unlike openat(2), unknown bits in @flags result in -EINVAL rather
* than being silently ignored. @mode must be zero unless one of {O_CREAT,
* O_TMPFILE} are set.
*
* @flags: O_* flags.
* @mode: O_CREAT/O_TMPFILE file mode.
* @resolve: RESOLVE_* flags.
*/
struct open_how {
__u64 flags;
__u64 mode;
__u64 resolve;
};
#define OPEN_HOW_SIZE_VER0 24 /* sizeof first published struct */
#define OPEN_HOW_SIZE_LATEST OPEN_HOW_SIZE_VER0
bool needs_openat2(const struct open_how *how);
#ifndef RESOLVE_IN_ROOT
/* how->resolve flags for openat2(2). */
#define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings
(includes bind-mounts). */
#define RESOLVE_NO_MAGICLINKS 0x02 /* Block traversal through procfs-style
"magic-links". */
#define RESOLVE_NO_SYMLINKS 0x04 /* Block traversal through all symlinks
(implies OEXT_NO_MAGICLINKS) */
#define RESOLVE_BENEATH 0x08 /* Block "lexical" trickery like
"..", symlinks, and absolute
paths which escape the dirfd. */
#define RESOLVE_IN_ROOT 0x10 /* Make all jumps to "/" and ".."
be scoped inside the dirfd
(similar to chroot(2)). */
#endif /* RESOLVE_IN_ROOT */
#define E_func(func, ...) \
do { \
if (func(__VA_ARGS__) < 0) \
ksft_exit_fail_msg("%s:%d %s failed\n", \
__FILE__, __LINE__, #func);\
} while (0)
#define E_asprintf(...) E_func(asprintf, __VA_ARGS__)
#define E_chmod(...) E_func(chmod, __VA_ARGS__)
#define E_dup2(...) E_func(dup2, __VA_ARGS__)
#define E_fchdir(...) E_func(fchdir, __VA_ARGS__)
#define E_fstatat(...) E_func(fstatat, __VA_ARGS__)
#define E_kill(...) E_func(kill, __VA_ARGS__)
#define E_mkdirat(...) E_func(mkdirat, __VA_ARGS__)
#define E_mount(...) E_func(mount, __VA_ARGS__)
#define E_prctl(...) E_func(prctl, __VA_ARGS__)
#define E_readlink(...) E_func(readlink, __VA_ARGS__)
#define E_setresuid(...) E_func(setresuid, __VA_ARGS__)
#define E_symlinkat(...) E_func(symlinkat, __VA_ARGS__)
#define E_touchat(...) E_func(touchat, __VA_ARGS__)
#define E_unshare(...) E_func(unshare, __VA_ARGS__)
#define E_assert(expr, msg, ...) \
do { \
if (!(expr)) \
ksft_exit_fail_msg("ASSERT(%s:%d) failed (%s): " msg "\n", \
__FILE__, __LINE__, #expr, ##__VA_ARGS__); \
} while (0)
int raw_openat2(int dfd, const char *path, void *how, size_t size);
int sys_openat2(int dfd, const char *path, struct open_how *how);
int sys_openat(int dfd, const char *path, struct open_how *how);
int sys_renameat2(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath, unsigned int flags);
int touchat(int dfd, const char *path);
char *fdreadlink(int fd);
bool fdequal(int fd, int dfd, const char *path);
extern bool openat2_supported;
#endif /* __RESOLVEAT_H__ */
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include "../kselftest.h"
#include "helpers.h"
/*
* O_LARGEFILE is set to 0 by glibc.
* XXX: This is wrong on {mips, parisc, powerpc, sparc}.
*/
#undef O_LARGEFILE
#define O_LARGEFILE 0x8000
struct open_how_ext {
struct open_how inner;
uint32_t extra1;
char pad1[128];
uint32_t extra2;
char pad2[128];
uint32_t extra3;
};
struct struct_test {
const char *name;
struct open_how_ext arg;
size_t size;
int err;
};
#define NUM_OPENAT2_STRUCT_TESTS 7
#define NUM_OPENAT2_STRUCT_VARIATIONS 13
void test_openat2_struct(void)
{
int misalignments[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 17, 87 };
struct struct_test tests[] = {
/* Normal struct. */
{ .name = "normal struct",
.arg.inner.flags = O_RDONLY,
.size = sizeof(struct open_how) },
/* Bigger struct, with zeroed out end. */
{ .name = "bigger struct (zeroed out)",
.arg.inner.flags = O_RDONLY,
.size = sizeof(struct open_how_ext) },
/* TODO: Once expanded, check zero-padding. */
/* Smaller than version-0 struct. */
{ .name = "zero-sized 'struct'",
.arg.inner.flags = O_RDONLY, .size = 0, .err = -EINVAL },
{ .name = "smaller-than-v0 struct",
.arg.inner.flags = O_RDONLY,
.size = OPEN_HOW_SIZE_VER0 - 1, .err = -EINVAL },
/* Bigger struct, with non-zero trailing bytes. */
{ .name = "bigger struct (non-zero data in first 'future field')",
.arg.inner.flags = O_RDONLY, .arg.extra1 = 0xdeadbeef,
.size = sizeof(struct open_how_ext), .err = -E2BIG },
{ .name = "bigger struct (non-zero data in middle of 'future fields')",
.arg.inner.flags = O_RDONLY, .arg.extra2 = 0xfeedcafe,
.size = sizeof(struct open_how_ext), .err = -E2BIG },
{ .name = "bigger struct (non-zero data at end of 'future fields')",
.arg.inner.flags = O_RDONLY, .arg.extra3 = 0xabad1dea,
.size = sizeof(struct open_how_ext), .err = -E2BIG },
};
BUILD_BUG_ON(ARRAY_LEN(misalignments) != NUM_OPENAT2_STRUCT_VARIATIONS);
BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_STRUCT_TESTS);
for (int i = 0; i < ARRAY_LEN(tests); i++) {
struct struct_test *test = &tests[i];
struct open_how_ext how_ext = test->arg;
for (int j = 0; j < ARRAY_LEN(misalignments); j++) {
int fd, misalign = misalignments[j];
char *fdpath = NULL;
bool failed;
void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
void *copy = NULL, *how_copy = &how_ext;
if (!openat2_supported) {
ksft_print_msg("openat2(2) unsupported\n");
resultfn = ksft_test_result_skip;
goto skip;
}
if (misalign) {
/*
* Explicitly misalign the structure copying it with the given
* (mis)alignment offset. The other data is set to be non-zero to
* make sure that non-zero bytes outside the struct aren't checked
*
* This is effectively to check that is_zeroed_user() works.
*/
copy = malloc(misalign + sizeof(how_ext));
how_copy = copy + misalign;
memset(copy, 0xff, misalign);
memcpy(how_copy, &how_ext, sizeof(how_ext));
}
fd = raw_openat2(AT_FDCWD, ".", how_copy, test->size);
if (test->err >= 0)
failed = (fd < 0);
else
failed = (fd != test->err);
if (fd >= 0) {
fdpath = fdreadlink(fd);
close(fd);
}
if (failed) {
resultfn = ksft_test_result_fail;
ksft_print_msg("openat2 unexpectedly returned ");
if (fdpath)
ksft_print_msg("%d['%s']\n", fd, fdpath);
else
ksft_print_msg("%d (%s)\n", fd, strerror(-fd));
}
skip:
if (test->err >= 0)
resultfn("openat2 with %s argument [misalign=%d] succeeds\n",
test->name, misalign);
else
resultfn("openat2 with %s argument [misalign=%d] fails with %d (%s)\n",
test->name, misalign, test->err,
strerror(-test->err));
free(copy);
free(fdpath);
fflush(stdout);
}
}
}
struct flag_test {
const char *name;
struct open_how how;
int err;
};
#define NUM_OPENAT2_FLAG_TESTS 23
void test_openat2_flags(void)
{
struct flag_test tests[] = {
/* O_TMPFILE is incompatible with O_PATH and O_CREAT. */
{ .name = "incompatible flags (O_TMPFILE | O_PATH)",
.how.flags = O_TMPFILE | O_PATH | O_RDWR, .err = -EINVAL },
{ .name = "incompatible flags (O_TMPFILE | O_CREAT)",
.how.flags = O_TMPFILE | O_CREAT | O_RDWR, .err = -EINVAL },
/* O_PATH only permits certain other flags to be set ... */
{ .name = "compatible flags (O_PATH | O_CLOEXEC)",
.how.flags = O_PATH | O_CLOEXEC },
{ .name = "compatible flags (O_PATH | O_DIRECTORY)",
.how.flags = O_PATH | O_DIRECTORY },
{ .name = "compatible flags (O_PATH | O_NOFOLLOW)",
.how.flags = O_PATH | O_NOFOLLOW },
/* ... and others are absolutely not permitted. */
{ .name = "incompatible flags (O_PATH | O_RDWR)",
.how.flags = O_PATH | O_RDWR, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_CREAT)",
.how.flags = O_PATH | O_CREAT, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_EXCL)",
.how.flags = O_PATH | O_EXCL, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_NOCTTY)",
.how.flags = O_PATH | O_NOCTTY, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_DIRECT)",
.how.flags = O_PATH | O_DIRECT, .err = -EINVAL },
{ .name = "incompatible flags (O_PATH | O_LARGEFILE)",
.how.flags = O_PATH | O_LARGEFILE, .err = -EINVAL },
/* ->mode must only be set with O_{CREAT,TMPFILE}. */
{ .name = "non-zero how.mode and O_RDONLY",
.how.flags = O_RDONLY, .how.mode = 0600, .err = -EINVAL },
{ .name = "non-zero how.mode and O_PATH",
.how.flags = O_PATH, .how.mode = 0600, .err = -EINVAL },
{ .name = "valid how.mode and O_CREAT",
.how.flags = O_CREAT, .how.mode = 0600 },
{ .name = "valid how.mode and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR, .how.mode = 0600 },
/* ->mode must only contain 0777 bits. */
{ .name = "invalid how.mode and O_CREAT",
.how.flags = O_CREAT,
.how.mode = 0xFFFF, .err = -EINVAL },
{ .name = "invalid (very large) how.mode and O_CREAT",
.how.flags = O_CREAT,
.how.mode = 0xC000000000000000ULL, .err = -EINVAL },
{ .name = "invalid how.mode and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR,
.how.mode = 0x1337, .err = -EINVAL },
{ .name = "invalid (very large) how.mode and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR,
.how.mode = 0x0000A00000000000ULL, .err = -EINVAL },
/* ->resolve must only contain RESOLVE_* flags. */
{ .name = "invalid how.resolve and O_RDONLY",
.how.flags = O_RDONLY,
.how.resolve = 0x1337, .err = -EINVAL },
{ .name = "invalid how.resolve and O_CREAT",
.how.flags = O_CREAT,
.how.resolve = 0x1337, .err = -EINVAL },
{ .name = "invalid how.resolve and O_TMPFILE",
.how.flags = O_TMPFILE | O_RDWR,
.how.resolve = 0x1337, .err = -EINVAL },
{ .name = "invalid how.resolve and O_PATH",
.how.flags = O_PATH,
.how.resolve = 0x1337, .err = -EINVAL },
};
BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_FLAG_TESTS);
for (int i = 0; i < ARRAY_LEN(tests); i++) {
int fd, fdflags = -1;
char *path, *fdpath = NULL;
bool failed = false;
struct flag_test *test = &tests[i];
void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
if (!openat2_supported) {
ksft_print_msg("openat2(2) unsupported\n");
resultfn = ksft_test_result_skip;
goto skip;
}
path = (test->how.flags & O_CREAT) ? "/tmp/ksft.openat2_tmpfile" : ".";
unlink(path);
fd = sys_openat2(AT_FDCWD, path, &test->how);
if (test->err >= 0)
failed = (fd < 0);
else
failed = (fd != test->err);
if (fd >= 0) {
int otherflags;
fdpath = fdreadlink(fd);
fdflags = fcntl(fd, F_GETFL);
otherflags = fcntl(fd, F_GETFD);
close(fd);
E_assert(fdflags >= 0, "fcntl F_GETFL of new fd");
E_assert(otherflags >= 0, "fcntl F_GETFD of new fd");
/* O_CLOEXEC isn't shown in F_GETFL. */
if (otherflags & FD_CLOEXEC)
fdflags |= O_CLOEXEC;
/* O_CREAT is hidden from F_GETFL. */
if (test->how.flags & O_CREAT)
fdflags |= O_CREAT;
if (!(test->how.flags & O_LARGEFILE))
fdflags &= ~O_LARGEFILE;
failed |= (fdflags != test->how.flags);
}
if (failed) {
resultfn = ksft_test_result_fail;
ksft_print_msg("openat2 unexpectedly returned ");
if (fdpath)
ksft_print_msg("%d['%s'] with %X (!= %X)\n",
fd, fdpath, fdflags,
test->how.flags);
else
ksft_print_msg("%d (%s)\n", fd, strerror(-fd));
}
skip:
if (test->err >= 0)
resultfn("openat2 with %s succeeds\n", test->name);
else
resultfn("openat2 with %s fails with %d (%s)\n",
test->name, test->err, strerror(-test->err));
free(fdpath);
fflush(stdout);
}
}
#define NUM_TESTS (NUM_OPENAT2_STRUCT_VARIATIONS * NUM_OPENAT2_STRUCT_TESTS + \
NUM_OPENAT2_FLAG_TESTS)
int main(int argc, char **argv)
{
ksft_print_header();
ksft_set_plan(NUM_TESTS);
test_openat2_struct();
test_openat2_flags();
if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
ksft_exit_fail();
else
ksft_exit_pass();
}
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Author: Aleksa Sarai <cyphar@cyphar.com>
* Copyright (C) 2018-2019 SUSE LLC.
*/
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <syscall.h>
#include <limits.h>
#include <unistd.h>
#include "../kselftest.h"
#include "helpers.h"
/* Construct a test directory with the following structure:
*
* root/
* |-- a/
* | `-- c/
* `-- b/
*/
int setup_testdir(void)
{
int dfd;
char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";
/* Make the top-level directory. */
if (!mkdtemp(dirname))
ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
dfd = open(dirname, O_PATH | O_DIRECTORY);
if (dfd < 0)
ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
E_mkdirat(dfd, "a", 0755);
E_mkdirat(dfd, "b", 0755);
E_mkdirat(dfd, "a/c", 0755);
return dfd;
}
/* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
pid_t spawn_attack(int dirfd, char *a, char *b)
{
pid_t child = fork();
if (child != 0)
return child;
/* If the parent (the test process) dies, kill ourselves too. */
E_prctl(PR_SET_PDEATHSIG, SIGKILL);
/* Swap @a and @b. */
for (;;)
renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
exit(1);
}
#define NUM_RENAME_TESTS 2
#define ROUNDS 400000
const char *flagname(int resolve)
{
switch (resolve) {
case RESOLVE_IN_ROOT:
return "RESOLVE_IN_ROOT";
case RESOLVE_BENEATH:
return "RESOLVE_BENEATH";
}
return "(unknown)";
}
void test_rename_attack(int resolve)
{
int dfd, afd;
pid_t child;
void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
int escapes = 0, other_errs = 0, exdevs = 0, eagains = 0, successes = 0;
struct open_how how = {
.flags = O_PATH,
.resolve = resolve,
};
if (!openat2_supported) {
how.resolve = 0;
ksft_print_msg("openat2(2) unsupported -- using openat(2) instead\n");
}
dfd = setup_testdir();
afd = openat(dfd, "a", O_PATH);
if (afd < 0)
ksft_exit_fail_msg("test_rename_attack: failed to open 'a'\n");
child = spawn_attack(dfd, "a/c", "b");
for (int i = 0; i < ROUNDS; i++) {
int fd;
char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";
if (openat2_supported)
fd = sys_openat2(afd, victim_path, &how);
else
fd = sys_openat(afd, victim_path, &how);
if (fd < 0) {
if (fd == -EAGAIN)
eagains++;
else if (fd == -EXDEV)
exdevs++;
else if (fd == -ENOENT)
escapes++; /* escaped outside and got ENOENT... */
else
other_errs++; /* unexpected error */
} else {
if (fdequal(fd, afd, NULL))
successes++;
else
escapes++; /* we got an unexpected fd */
}
close(fd);
}
if (escapes > 0)
resultfn = ksft_test_result_fail;
ksft_print_msg("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d\n",
eagains, exdevs, other_errs, successes);
resultfn("rename attack with %s (%d runs, got %d escapes)\n",
flagname(resolve), ROUNDS, escapes);
/* Should be killed anyway, but might as well make sure. */
E_kill(child, SIGKILL);
}
#define NUM_TESTS NUM_RENAME_TESTS
int main(int argc, char **argv)
{
ksft_print_header();
ksft_set_plan(NUM_TESTS);
test_rename_attack(RESOLVE_BENEATH);
test_rename_attack(RESOLVE_IN_ROOT);
if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
ksft_exit_fail();
else
ksft_exit_pass();
}
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment