Skip to main content

Linux Kernel Internals - File operation in a character device driver

What does a user-space application generally do w.r.t the other file-system resource (other then interacting with user)?
Reading some configuration file, or flushing logs. 
Let's say we are browsing internet in our chrome, then chrome would be ultimately operating on the network socket files (there may be some hierarchy). 
Let's say we played some song in VLC player, ultimately our VLC will read the mp3 file, write the data to some audio device file, after which the audio driver will get the data and configs the corresponding CODEC register to get the audio output.

Now, for all the operation above, for an user-space the resources (mp3, device file, socket file, log file) will look like a file. Thus the application would do nothing more then calling open, read, write which are file based operations.

For a user-space application, a kernel device driver also looks like a file (/dev/demochardrv)
In this article we will focus on how to achieve these file based operation in driver level.

iv) File operation based initialization

Whatever file operation done on the device file is decoded to the driver level instruction by the VFS. Now to achieve this firstly driver need to register the file based operational subroutine(struct file_operations ) structure with the character device structure(struct cdev).

#include <linux/cdev.h>

void cdev_init(struct cdev *, const struct file_operations *);
struct file_operations contain pointers of the operational subroutines like open, read, write, close etc.

Next, the character device structure is handed over to the VFS.
int cdev_add(struct cdev *, dev_t, unsigned);
where second argument of type dev_t was previously registered/allocated, and third argument is the number of minor numbers associated with the device.

v) User space address accessed by the kernel

The memory address region for the kernel is different from that of the user space application. The kernel don't know the address space of the user level application. In that case any memory pointer passed by the user space application for read/write operation cannot be directly accessed by the kernel. Thus any memory related direct operation like memcpy, could possibly result in a illegal memory access. 
Two API's is used by the kernel to operate on user space address:
unsigned long copy_to_user (void __user *to, const void *from, unsigned long n);
will write to the user space address.

unsigned long copy_from_user (void *to, const void __user *from, unsigned long n);
will read from user space address

In Unix perspective a file based operation may be reading from the file, or writing to the file. Let's say we have implemented the read subroutine that shall read some CPU register value into the user space buffer, or let's say we have implemented the write subroutine that write the user level buffer into the CPU register bank. Till this stage the requirement is fulfilled.
Now Let's say the user application need to communicate with the driver. Take an example of a crypto driver that will take the encrypted data and decrypt them. To support this functionality we need to have some interface in which both read and write could be done. Unix provide this interface known as IOCTL (Input Output Control).

long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

#include <linux/module.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include "demo.h"

static dev_t demo_devt;
struct class *cl;
struct cdev demo_cdev;

int demo_open (struct inode *i, struct file *f)
{
printk(KERN_INFO "%s \n", __func__);
return 0;
}

int demo_close (struct inode *i, struct file *f)
{
printk(KERN_INFO "%s \n", __func__);
return 0;
}

static int my_strrev(struct demostr __user *arg)
{
struct demostr st;
int len, i;
char c;
// hello[] = "hello";

if (copy_from_user(&st, arg, sizeof(st))) {
printk(KERN_ERR"copy_from_user Failed !!!\n");
return -EFAULT;
}

for (len=0; st.str[len]; len++);
printk(KERN_INFO "received string [%s] len = %d\n", st.str, len);

for(i=0; i < len/2; i++) {
c = st.str[i];
st.str[i] = st.str[len - i - 1];
st.str[len - i - 1] = c;
}

//memcpy(st.str, hello, sizeof(hello));
if (copy_to_user(arg, &st, sizeof(st))) {
printk(KERN_ERR"copy_to_user Failed !!!\n");
return -EFAULT;
}

return 0;

}

static long demo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
void __user *uarg = (void __user *)arg;

printk(KERN_INFO "%s \n", __func__);
switch(cmd) {
case CMD_DEMO_STRREV:
return my_strrev(uarg);
default:
printk(KERN_ERR "IOCTL unknown command %d \n", cmd);
}

return -EINVAL;
}

static struct file_operations demo_fops = {
.owner = THIS_MODULE,
.open = demo_open,
.release = demo_close,
.unlocked_ioctl = demo_ioctl,
.compat_ioctl = demo_ioctl,
};

static int module_constructor(void) /* Constructor */
{
int ret;
struct device *dev_ret;

        printk(KERN_INFO "Initializing demo module\n");
ret = alloc_chrdev_region(&demo_devt, 0, 3, "demodevt");
if (ret) {
printk(KERN_ERR "failed to allocate char device\n");
return ret;
}

printk(KERN_INFO "Major = %d minor = %d\n", MAJOR(demo_devt), MINOR(demo_devt));

if (IS_ERR(cl = class_create(THIS_MODULE, "democharcl"))) {
printk(KERN_ERR "failed to create class\n");
unregister_chrdev_region(demo_devt, 1);
return PTR_ERR(cl);
}

if (IS_ERR(dev_ret = device_create(cl, NULL, demo_devt, NULL, "demochardrv"))) {
printk(KERN_ERR "failed to create device\n");
class_destroy(cl);
unregister_chrdev_region(demo_devt, 1);
return PTR_ERR(dev_ret);
}

cdev_init(&demo_cdev, &demo_fops);

ret = cdev_add(&demo_cdev, demo_devt, 3);
if (ret) {
printk(KERN_ERR "failed to cdev_add\n");
device_destroy(cl, demo_devt);
class_destroy(cl);
unregister_chrdev_region(demo_devt, 1);

}

printk(KERN_INFO"DEMO module initialization SUCCESS\n");
        return 0;
}

static void module_destructor(void) /* Destructor */
{

        printk(KERN_INFO "Deinitializing the module\n");
device_destroy(cl, demo_devt);
class_destroy(cl);
unregister_chrdev_region(demo_devt, 3);
}

module_init(module_constructor);
module_exit(module_destructor);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Sourabh Das <sourabhemsec@outlook.com>");
MODULE_DESCRIPTION("Our First Character Driver");


#include <iostream>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include "demo.h"

const char devfile[] = "/dev/demochardrv";
using namespace std;

int main()
{
        int fd, ret;
        struct demostr st;
        cout << "HELLO WORLD" << endl;
        fd = open(devfile, O_RDWR);
        if (fd < 0) {
                cout << "ERROR :: reading file" << endl;
                return -1;
        }
        strcpy(st.str, "HELLO WORLD");        ret = ioctl(fd, CMD_DEMO_STRREV, &st);
        if (ret < 0) {
                cout << "ERROR IOCTL !!!" << ret << endl;
        } else {
                cout << "SUCCESS IOCTL" << st.str << endl;
                printf("return = %s\n", st.str);
        }
        close(fd);
        return 0;
}

Comments

Popular posts from this blog

ARM Trustzone - An overview on how SMC calls are handled by the EL3 Monitor

In this write up, we will focus mainly on the ARMv8-A exceptions, the role of ARM Trusted Firmware (that provides Secure Monitor functionality) and how the World Switch happens between Secure and Normal. If we look on the the architectural diagram of ARM Trustzone w.r.t ARMv8-A, the Execution Level is divided into four levels namely: EL0 (Secure & Non-Secure) - User Application EL1 (Secure & Non-Secure) - Kernel EL2 - Hypervisor for running different OS's simuntaneously EL3 - Security Monitor Now, whenever a normal world User Application calls for some Secure Operation, the calls goes via IOCTL call to the Linux Driver, which ultimately calls the smc instruction. To understand what the smc instruction, we have to look on the Exceptions in ARMv8 ARMv8 Exceptions In ARMv8 the exceptions are divided into two categories: Synchronous & Asynchronous.  An exception is described as synchronous if it is generated as a result of execution or attempted executi...

An overview of ARM Memory Management Unit

The scope of this documentation is to understand the Memory Management Unit for ARMv8 Based processor. Memory management Unit converts the virtual Address (in CPU's logical space) into Physical Address. For an example let us suppose in the following program: int variable; printf("Addrss of variable = 0x%x\n", &variable); The address could be anything (Let's assume  0x40000200 ). Now 0x40000200 may or may not the actual memory address in the Physical Memory (RAM). It could be anything thing (lets assume  0xA0000200 ). Thus the CPU produce the logical address 0x40000200 which is converted into the physical address 0xA0000200 by the Memory Management Unit. Now the question remains Why we require an Address Translation, or in other word in the above program why we don't operate on actual physical memory 0xA0000200? Let us suppose a program that requires a huge amount of contagious memory in the RAM. Now our external memory would have that much memory requ...

Setting my Yocto qemu environment for reverse engineering experimental purpose

In this post I have discussed about, how I set my ARM reverse engineering platform in Yocto Qemu. Generally when we are talking about reverse engineering then we need a target platform where we could exercise our experiments. We Can choose Raspberry pi, Beagle Bone etc. for these kind of experimentation. But what we can do in these platforms, can also be performed in some virtual environments. Only for experimenting on some kind of side channel attacks, we would need the actual hardware. For making the setup, you would need some PC with very good configuration likely atleast 4 GB of RAM, 100 GB of free space, and with atleast Quad Core Processor. I have installed VMWare (non-commercial version) which is running Ubuntu 18.04. If you have ubuntu installed in your PC itself then it is well and good. I have followed this link to install Ubuntu. The following steps would help: 1. First clone the source code of Yocto. You might also require some dependencies to get it installed: $ c...