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

  1. https://github.com/saurvs/rperm-netbsd
  2. http://saurvs.github.io/post/writing-netbsd-kern-mod/
  3. https://wiki.netbsd.org/rumpkernel/
  4. https://nxr.netbsd.org/xref/src/sys/sys/conf.h
  5. http://netbsd.gw.com/cgi-bin/man-cgi?uiomove+9+NetBSD-current
  6. http://netbsd.gw.com/cgi-bin/man-cgi?cprng_fast+9+NetBSD-current