Featured image

How to debug a Linux Kernel Module

Build an example module

Let’s start by write a simple test module. We can try the Hello World example from cyberciti.biz You first have to install the kernel header files with apt-get install kernel-headers-$(uname -r).

Content of hello.c:

#include <linux/module.h>
#include <linux/kernel.h>

int init_module(void)
{
    printk(KERN_INFO "init_module() called\n");
    return 0;
}

void cleanup_module(void)
{
    printk(KERN_INFO "cleanup_module() called\n");
}

MODULE_LICENSE("GPL");

Content of Makefile:

obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
    $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
clean:
    rm -rf *.ko
    rm -rf *.mod.c
    rm -rf *mod.o
    rm -rf *.o

Build and load, and unload the module:

kmille@linbox: make  
kmille@linbox: insmod hello.ko  
kmille@linbox: rmmod hello.ko  

You will find the proper messages in the dmesg output:

kmille@linbox: dmesg:  
...
kmille@linbox: [  512.003876] init_module() called  
kmille@linbox: [  512.009321] cleanup_module() called

Debugging a real module

For example nf_log_ipv4 which gets called if iptables -j TRACE is executed.

  1. Get the source code of the module
    • download it from Github: git clone https://github.com/torvalds/linux
    • checkout the kernel version you are currently running: git checkout ...
  2. Add multiple print statements
    • printk(KERN_INFO "nf_log_ipv4: Passed %s %d \n",__FUNCTION__,__LINE__);
  3. In the Makefile above replace hello.o with nf_log_ipv4.o
    • that’s it. just use make to build it
  4. Load the module into the kernel with insmod nf_log_ipv4
    • insmod does not load dependencies => symbol not found error
    • modprobe automatically loads dependencies
    • but modprobe loads modules from /lib/modules/$(uname -r)/, insmod from the current dir.

My actual problem

I was debugging why iptables -j TRACE didn’t log something to dmesg. After some time I found out why: Debian buster moved from netfilter to nf_tables per default and you get the logs via nft monitor trace. If you use the old iptables (/usr/sbin/iptables-legacy) the messages are logged to dmesg.

Automate the process

Adding prints to debug code is a very old school approach. A more modern way would be function tracing (for example with bpftrace). If we do the classic print way, we should automate the build and debug process. Content of rebuild.sh:

root@buster:~/mod/hello-example# cat rebuild.sh 
#/bin/bash
#iptables=/usr/sbin/iptables-legacy
iptables=iptables

make clean
make
rmmod nf_log_ipv4
modprobe nf_log_common
insmod nf_log_ipv4.ko
echo nf_log_ipv4 > /proc/sys/net/netfilter/nf_log/2
cat /proc/net/netfilter/nf_log
$iptables -t raw -F
$iptables -t raw -I OUTPUT -p icmp -j TRACE
$iptables -t raw -I PREROUTING -p icmp -j TRACE
lsmod | grep nf_log
ping -c 1 1.1.1.1
$iptables -t raw -vnL

Where to look in the code?

It’s very easy to grep the kernel code as everything is in a single namespace and all functions have a unique name. You can use strace to get the called syscalls which is a good starting point:

root@linbox ~ strace -e network iptables -vnL
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 4
getsockopt(4, SOL_IP, IPT_SO_GET_INFO, "filter\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., [84]) = 0
getsockopt(4, SOL_IP, IPT_SO_GET_ENTRIES, "filter\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., [2984]) = 0
Chain INPUT (policy DROP 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, 0x7fff31ce9a90, [30]) = -1 EPROTONOSUPPORT (Protocol not supported)
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, "conntrack\0\202\365d\177\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1", [30]) = 0
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, 0x7fff31ce9a40, [30]) = -1 EPROTONOSUPPORT (Protocol not supported)
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, "conntrack\0\214\365d\177\0\0\1\0\0\0\0\0\0\0\3111\200\365d\2", [30]) = 0
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, 0x7fff31ce99f0, [30]) = -1 EPROTONOSUPPORT (Protocol not supported)
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, "conntrack\0\227\342\332y\347X\1\0\0\0\0\0\0\0\3111\200\365d\3", [30]) = 0
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, 0x7fff31ce99a0, [30]) = -1 EPROTONOSUPPORT (Protocol not supported)
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, "conntrack\0\214\365d\177\0\0\0\0\0\0\0\0\0\0\3111\200\365\1\3", [30]) = 0
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, "conntrack\0\214\365d\177\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\3", [30]) = 0
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, "conntrack\0\214\365d\177\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1\2", [30]) = 0
    0     0 DROP       all  --  *      *       0.0.0.0/0            0.0.0.0/0            state INVALID
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, 0x7fff31ce9d00, [30]) = -1 EPROTONOSUPPORT (Protocol not supported)
1472K 1056M ACCEPT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
 1619 97140 ACCEPT     all  --  lo     *       0.0.0.0/0            0.0.0.0/0
    0     0 ACCEPT     icmp --  *      *       0.0.0.0/0            0.0.0.0/0
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_TARGET, "REJECT\0\0s\371\202\365d\177\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", [30]) = 0
    0     0 REJECT     all  --  cdark.net *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-port-unreachable
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, "tcp\0d\177\0\0s\371\202\365d\177\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", [30]) = 0
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22000
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)  = 5
getsockopt(5, SOL_IP, IPT_SO_GET_REVISION_MATCH, "udp\0d\177\0\0s\371\202\365d\177\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", [30]) = 0
    0     0 ACCEPT     udp  --  *      *       0.0.0.0/0            0.0.0.0/0            udp dpt:21027
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:5001
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080
   63 17777 REJECT     all  --  *      *       0.0.0.0/0            0.0.0.0/0            reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 398K packets, 4393M bytes)
 pkts bytes target     prot opt in     out     source               destination
+++ exited with 0 +++
root@linbox ~ #

If you are looking for the IPT_SO_GET_INFO function,

Update: I also wrote about tracing kernel functions with bpftrace.