Testing-a-feature-as-a-module should not really exist. You should be able to
test the feature, if it is present. – Someone(tm)
After working on writing unit tests for uvm_hotplug(9) I was quite fascinated by
the prospects of testing a kernel module in userspace using similar techniques.
After looking at rperm[1] and saurvs’ excellent explanation[2] on how Kernel
modules work. I thought it would be a nice starting point to try out ATF based
testing of kernel modules in userland.
NOTE: Before go further, do understand that this is my first attempt at trying
to do testing of Kernel modules in userland and yes I am aware of things like
rumpkernel[3] that is able to run an “userland NetBSD Kernel” where one could
test out kernel modules and drivers in userland. But that is not what I intend
to cover here, but a methodical way of writing unit tests for the functional
part of the kernel modules. Again I have not gone through in detail actual real
world kernel modules so this is “experimental” at best.
Creating the ATF template
Initial export of uvm_hotplug(9) APIs was done by cherry@ and following in
similar footsteps I copied over the template and named it as t_rperm.c
in a
new tests/
folder under the forked github repository.
Other than the standard set of headers, some debug and diagnostic macros, the
first steps was to bring out the sys/conf.h
[4] device access wrappers to
userland. Directly including this file would not help since the section having
these macros are put under the #ifdef _KERNEL
section. And trying to define it
via -D
flag in a compiler would be a clusterfuck of dependencies.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define dev_type_open(n) int n (dev_t, int, int, struct lwp *)
#define dev_type_close(n) int n (dev_t, int, int, struct lwp *)
#define dev_type_read(n) int n (dev_t, struct uio *, int)
#define dev_type_write(n) int n (dev_t, struct uio *, int)
#define dev_type_ioctl(n) \
int n (dev_t, u_long, void *, int, struct lwp *)
#define dev_type_stop(n) void n (struct tty *, int)
#define dev_type_tty(n) struct tty * n (dev_t)
#define dev_type_poll(n) int n (dev_t, int, struct lwp *)
#define dev_type_mmap(n) paddr_t n (dev_t, off_t, int)
#define dev_type_strategy(n) void n (struct buf *)
#define dev_type_dump(n) int n (dev_t, daddr_t, void *, size_t)
#define dev_type_size(n) int n (dev_t)
#define dev_type_kqfilter(n) int n (dev_t, struct knote *)
#define dev_type_discard(n) int n (dev_t, off_t, off_t)
#define noopen ((dev_type_open((*)))enodev)
#define noclose ((dev_type_close((*)))enodev)
#define noread ((dev_type_read((*)))enodev)
#define nowrite ((dev_type_write((*)))enodev)
#define noioctl ((dev_type_ioctl((*)))enodev)
#define nostop ((dev_type_stop((*)))enodev)
#define notty NULL
#define nopoll seltrue
paddr_t nommap(dev_t, off_t, int);
#define nodump ((dev_type_dump((*)))enodev)
#define nosize NULL
#define nokqfilter seltrue_kqfilter
#define nodiscard ((dev_type_discard((*)))enodev)
#define seltrue 0
#define seltrue_kqfilter 0
The next order of business was to “empty” out unwanted sections of the code that
pertained to the specifics of writing up a kernel module.
We do this since it does not make sense to have this code in test frameworks
since it is note these parts of a module we intend to test. We intend to test
the correctness of logic which we are trying to implement in the module.
1
#define MODULE(MODULE_CLASS_DRIVER, rperm, NULL);
This is why MODULE
macro was decided to be taken out within the test file.
Next in line were the Kernel API provisioned functions that allows the devices
to be attached or detached.
1
2
3
4
5
static int
devsw_attach(const char *devname, const struct bdevsw *bev, devmajor_t *bmajor, const struct cdevsw *cdev, devmajor_t *cmajor)
{
return 0;
}
1
2
3
4
5
static int
devsw_detach(const struct bdevsw *bdev, const struct cdevsw *cdev)
{
return 0;
}
Another function I came across which needed an userspace implementation is
uiomove()
[5], which helps move data from / to “userspace” to / from “kernel
space” respectively. Of course one does not just throw about data in Kernels,
they use a struct to move it around and the struct also needed a userland
implementation so that one can “mock” the calls to uiomove()
.
A description of uiomove()
had the following statement, which I found fascinating
A struct uio typically describes data in motion.
because it led me to this imaginary world of data being transported in structs
like wagons in a railway to and from a secure location, which gave me this
particularly interesting heist movie plot where data is stolen while it is
motion. But I digress with my fantasies of heist and action on a moving train.
The following is a “mock” struct which was written for the userland testing.
1
2
3
4
5
6
7
struct uio {
struct iovec *uio_iov; /* pointer to array of iovecs */
int uio_iovcnt; /* number of iovecs in array */
off_t uio_offset; /* offset into file this uio corresponds to */
size_t uio_resid; /* residual i/o count */
enum uio_rw uio_rw; /* see above */
};
And the accompanying the function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int
uiomove(void *buf, size_t n, struct uio *uio)
{
switch(uio->uio_rw) {
case UIO_READ:
memcpy(buf, uio->uio_iov->iov_base, n);
break;
case UIO_WRITE:
memcpy(uio->uio_iov->iov_base, buf, n);
break;
default:
panic("Unknown operation %d.\n", (int)uio->uio_rw);
}
return 0;
}
As usual we have replaced most of the kernel space equivalents with their user
space equivalents.
The only remaining kernel function which we needed to replaced with userspace
equivalent implementation was a cryptographic pseudorandom number generator
cprng_strong32()
[6].
1
2
3
4
5
6
static uint32_t
cprng_strong32(void)
{
srandom((uint32_t)time(NULL));
return (uint32_t) random();
}
It is interesting to note that one can effectively build a good set of “mock”
APIs to emulate kernel equivalents instead of going through this every single
time.
Writing the SetUp()
I did not want to leave the setup()
empty and even though this is probably not
the ideal case I thought of giving a call to rperm_modcmd()
. Ideally the
setup()
should do the prerequisites before we start to test rperm
module. This will require further consultation with cherry@ on what exactly are
the things to do in the setup()
.
1
2
3
4
5
6
7
8
9
/*
* Test Fixture SetUp().
*/
static void
setup(void)
{
/* Prerequisites for running certain calls in rperm */
rperm_modcmd(0, NULL);
}
ATF test case
Finally I wrote up a sample test case. Of course it is a really trivial test
that I do here.
1
2
3
4
5
6
7
8
9
10
11
12
ATF_TC(rperm_get_open);
ATF_TC_HEAD(rperm_get_open, tc)
{
atf_tc_set_md_var(tc, "descr", "Tests if the rperm_open() works");
}
ATF_TC_BODY(rperm_get_open, tc)
{
setup();
int open_return = rperm_open(DEV_CMINOR, O_RDONLY, S_IFCHR, NULL);
ATF_CHECK_EQ(0, open_return);
}
The point is to show that one can with enough persistence, export a kernel
module into userspace and subject it to logical testing using the ATF
framework.
Conclusion
As you can see from the above, “Testing-a-feature-as-a-module” is not what I am
trying to do, on the contrary it is the “Testing-of-a-module-as-a-feature-of-ATF”.
More seriously, it is possible to bring kernel modules down to userspace with
the right amount of mocking APIs and definition to make it easier to do unit
test the logic of the various functions used in the module.
References
- https://github.com/saurvs/rperm-netbsd
- http://saurvs.github.io/post/writing-netbsd-kern-mod/
- https://wiki.netbsd.org/rumpkernel/
- https://nxr.netbsd.org/xref/src/sys/sys/conf.h
- http://netbsd.gw.com/cgi-bin/man-cgi?uiomove+9+NetBSD-current
- http://netbsd.gw.com/cgi-bin/man-cgi?cprng_fast+9+NetBSD-current