Managed PageFault in the Linux Kernel
- Tutorial
Further, on the example of PageFault, some features of the exception handling process will be considered, as well as a description of the method that allows you to use this feature when developing Linux kernel modules for the x86 architecture.
Core exceptions
As an example of where and how exceptions are used in the kernel, consider copying data between kernel space and user space. Usually, the copy_from_user and copy_to_user functions are responsible for this , the peculiarity of which is different from
memcpythe fact that they correctly handle exceptions that occur during data transfer between different address spaces. Indeed, if we consider the situation when data is copied from the kernel to the user (function
copy_to_user), then situations may arise when the page of the user process into which an attempt is made to write is in a swap or is inaccessible to the process at all. And if in the first case the correct solution to the problem is to load this page and continue copying, then in the second case it is necessary to interrupt the operation and return the error code to the user (for example, -EINVAL). Obviously, the execution of the command that addresses the address corresponding to the missing page causes an exception, namely, the exception of page failure , or Page Fault (
#PF). At this moment, the kernel saves the context of the current task and executes the code of the corresponding handler - do_page_fault. One way or another, fixing the problem, the kernel restores the context of the interrupted task. However, depending on the result of processing the exception, the return address may differ from the address of the instruction that was the cause of the exception. In other words, thanks to the mechanism provided in the kernel, it is possible to specify for the potentially “dangerous” instruction the address from which the work will continue if an exception is generated when it is executed.Exception handling interface
To understand how the indicated mechanism is implemented, it is worth considering the implementation of the primitive of copying 4 bytes from the kernel to the user - the __put_user_4 function :
62 ENTRY(__put_user_4)
63 ENTER
64 mov TI_addr_limit(%_ASM_BX),%_ASM_BX
65 sub $3,%_ASM_BX
66 cmp %_ASM_BX,%_ASM_CX
67 jae bad_put_user
68 ASM_STAC
69 3: movl %eax,(%_ASM_CX) <- здесь может возникнуть исключение
70 xor %eax,%eax
71 EXIT
72 ENDPROC(__put_user_4)
...
89 bad_put_user:
90 CFI_STARTPROC
91 movl $-EFAULT,%eax
92 EXIT
...
98 _ASM_EXTABLE(3b,bad_put_user)
As you can see, in addition to checking the address range, this function directly transfers data (instructions
movlon line 69). This is where exceptions can be expected, because in addition to the fact that the target address really belongs to the range of user space addresses, nothing more is known about it. Next, you should pay attention to the _ASM_EXTABLE macro , which is the following:43 # define _ASM_EXTABLE(from,to) \
44 .pushsection "__ex_table","a" ; \
45 .balign 8 ; \
46 .long (from) - . ; \
47 .long (to) - . ; \
48 .popsection
The action of this macro is to add
__ex_tabletwo values to the special section - fromand to, which are not difficult to notice, correspond with the addresses of the "suspicious" instruction on line 69 and the instruction with which execution will continue after the exception is processed, namely bad_put_user. Adding a record to the table __ex_tablemakes the point of failure manageable, because This table is used by the kernel when handling exceptions.Exception tables and their processing
So, as was noted, the exception table is the central place where information about those instructions is stored, the error during the execution of which must be processed separately. Looking ahead, it is worth noting that in addition to the table of the kernel itself, an individual table is also provided for each module. However, now it’s worth considering the structure of its element, described by the exception_table_entry structure :
97 struct exception_table_entry {
98 int insn, fixup;
99 };
As you can see, the format of the table element corresponds to what was revealed when considering the macro
_ASM_EXTABLE. The first element describes the instruction, the second describes the code to which control will be transferred in case of an exception. Each time a page failure exception occurs, the Linux kernel checks, among other things, whether the address of the command that caused this exception is contained in the __ex_tablekernel table , or in one of the tables of loaded modules. If such a record is found, then the corresponding action is performed. Otherwise, the kernel executes some standard logic to complete the exception handling. As for the individual exception tables of kernel modules, the format of the elements of these tables is standard and corresponds to that for the kernel. A link to such a table for each module is available by pointer
THIS_MODULE->extablewhile the number of table elements is contained in the variable THIS_MODULE->num_exentries. The THIS_MODULE macro itself provides a link to the module descriptor structure:223 struct module
224 {
...
276 /* Exception table */
277 unsigned int num_exentries;
278 struct exception_table_entry *extable;
...
378 };
The following is a key kernel function that searches for a handler that matches the instruction that caused the exception. Here is her code :
50 /* Given an address, look for it in the exception tables. */
51 const struct exception_table_entry *search_exception_tables(unsigned long addr)
52 {
53 const struct exception_table_entry *e;
54
55 e = search_extable(__start___ex_table, __stop___ex_table-1, addr);
56 if (!e)
57 e = search_module_extables(addr);
58 return e;
59 }
As you can see, indeed, first of all, the search is performed in the base table of the kernel
__ex_tableand only then, in the absence of a result, it continues among the module exception tables. If none of the handlers matches the address of the instruction, the result of the kernel executing this function will be NULL. Otherwise, the result will be a pointer to the corresponding element of the exception table.Exception handling in the kernel module
So, if the general procedure for handling exceptions is clear, then you can create a module for training, the purpose of which will be to create and process exceptions. The code I already wrote is available on github . Below I will give a brief description of the code and give some comments.
So, let the function that makes the regular NULL pointer dereference deal with the generation of PageFault exceptions:
static void raise_page_fault(void)
{
debug(" %s enter\n", __func__);
((int *)0)[0] = 0xdeadbeef;
debug(" %s leave\n", __func__);
}
Obviously, trying to write to a null pointer will lead to a crash. And this is exactly what you need. In order to properly respond, you must:
- determine the address of the instruction causing the exception
- create a valid item
exception_table_entry - add the created element to the
extablemodule table
The following is a function that performs the above actions using disassembly using udis86 :
static int fixup_page_fault(struct exception_table_entry * entry)
{
ud_t ud;
ud_initialize(&ud, BITS_PER_LONG, \
UD_VENDOR_ANY, (void *)raise_page_fault, 128);
while (ud_disassemble(&ud) && ud.mnemonic != UD_Iret) {
if (ud.mnemonic == UD_Imov && \
ud.operand[0].type == UD_OP_MEM && ud.operand[1].type == UD_OP_IMM)
{
unsigned long address = \
(unsigned long)raise_page_fault + ud_insn_off(&ud);
extable_make_insn(entry, address);
extable_make_fixup(entry, address + ud_insn_len(&ud));
return 0;
}
}
return -EINVAL;
}
As you can see, the first step is to configure the disassembler (beginning of the analysis -
raise_page_fault). Next, with a given search depth, iterate over the commands. The desired command (what the operation is translated into ((int *)0)[0] = 0xdeadbeef;) is an ordinary one movl $0xdeadbeef, 0with the first operand of type UD_OP_MEMand the second of type UD_OP_IMM. As soon as the command address is found, the table element is formed. At the same time, the following functions are performed:static void extable_make_insn(struct exception_table_entry * entry, unsigned long addr)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,5,0)
entry->insn = (unsigned int)((addr - (unsigned long)&entry->insn));
#else
entry->insn = addr;
#endif
}
static void extable_make_fixup(struct exception_table_entry * entry, unsigned long addr)
{
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,5,0)
entry->fixup = (unsigned int)((addr - (unsigned long)&entry->fixup));
#else
entry->fixup = addr;
#endif
}
The first of them forms the address of the instruction in the structure. The second is the fixap address, i.e. the command to which control will be transferred. It is important to note that starting from kernel 3.5
exception_table_entry, small changes have occurred in the structure , namely, the dimension of its fields has been reduced - insnand fixupfor 64-bit architectures. This allowed us to reduce the amount of memory required for storing addresses, but the calculation logic changed slightly. Thus, after 3.5 nucleus, fields insnand fixupstore 32-bit values corresponding to displacements relative addresses of data elements. For those who are interested in bringing a commit that ruined everything 706276543b699d80f546e45f8b12574e7b18d952 .Conclusion
This example demonstrates the ability to manage exception handling in the Linux kernel using the kernel module. In a test example, an exception (PageFault) was called in a pre-prepared environment, namely, a configured
exablesmodule table . The latter circumstance made it possible to eliminate the abnormal termination and continue the execution of the program with the command following the emergency instruction. In addition, the prepared test case allows us to evaluate the possibility of handling some other exceptions, such as division error (#DE) and undefined opcode (#UD):
struct {
const char * name;
int (* fixup)(struct exception_table_entry *);
void (* raise)(void);
} exceptions[] = {
{
.name = "0x00 - div0 error (#DE)",
.fixup = fixup_div0_error,
.raise = raise_div0_error,
},
{
.name = "0x06 - undefined opcode (#UD)",
.fixup = fixup_undefined_opcode,
.raise = raise_undefined_opcode,
},
{
.name = "0x14 - page fault (#PF)",
.fixup = fixup_page_fault,
.raise = raise_page_fault,
},
};