Static verification for memory safety of Linux kernel drivers*

. Memory errors in Linux kernel drivers are a kind of serious bugs that can lead to dangerous consequences but such errors are hard to detect. This article describes static verification that aims at finding all errors under certain assumptions. Static verification of industrial projects such as the Linux kernel requires additional effort. Limitations of current tools for static verification disallow to analyze the Linux kernel as a whole, so we use a simplified automatically generated environment model. This model introduces inaccuracy, but provides ability for verification. In addition, we allow absent definitions for some functions which results in incomplete ANSI C programs. The current work proposes an approach to reveal issues with memory usage in such incomplete programs. Our static verification technique is based on Symbolic Memory Graphs (SMG) with extensions aiming to reduce a false alarm rate. We introduced an on-demand memory conception for simplification of kernel API models and implemented this conception in static verification tool CPAchecker. Also, we changed precision of a CPAchecker memory model from bytes to bits and supported structure alignment similar to the GCC compiler. We implemented the predicate extension for SMG to improve accuracy of the analysis. We verified of Linux kernel 4.11.6 and 4.16.10 with help of the Klever verification framework with CPAchecker as a verification engine. Manual analysis of warnings produced by Klever revealed 78 real bugs in drivers. We have made patches to fix 33 of them.


Introduction
Operating system kernels are often written in the C programming language.This language is portable and effective, but unfortunately it is not memory safe.Memory issues can lead to vulnerabilities or unpredictable failures.Common methods such as testing are unable to find all problems.A probable solution to get an evidence of satisfiability of safety properties is formal methods and there are results of comprehensive formal verification of the seL4 microkernel [1].However formal methods generally require a whole program and a complete model of its environment to produce an appropriate verdict.For example, Microsoft developed Static Driver Verifier (SDV) [2] to improve Microsoft Windows stability.SDV contains models of the kernel and drivers' environment, and over 60 API usage rules.The Linux kernel is important open source software.There are many research and industrial projects for improving kernel quality by verification, testing, bug hunting, fuzzing and error reports.Coverity [3], Saturn [4], DDVerify [5], Coccinelle [6], Linux Driver Verification [7] are projects which work on improving Linux stability.This article considers operating system kernel drivers with automatically generated environment models as a target for approbation of a memory verification technology.Main contributions of the paper are connected with extensions of an existed static memory verification approach to be able to perform Linux kernel drivers verification, which are described in Section 4.

Linux driver verification
The Linux kernel represents an industrial code base with more than 10 million lines of drivers' code.A distinctive feature of Linux is instability of internal interfaces.A high speed of changes with a distributed development process requires an efficient bug finding strategy.The research of faults in Linux operating system drivers divides errors into typical and specific [8].Specific faults in drivers are described as connected with hardware and not applicable to other drivers.Typical faults can be specified by some rule which is true for all or some group of drivers.Typical faults are further divided into:  Linux specific faults, which correspond to rules of correct usage of the Linux kernel API;  races and deadlocks, which are related with parallel execution;  generic problems, which are common for C programs such as null pointer dereference, integer overflow, etc. Authors show that 29.2% of typical errors fixed in stable branches of the Linux kernel are generic problems.Statistics of memory problems corresponding to all generic faults is shown in Table .1.

Symbolic memory graphs
The symbolic memory graph (SMG) algorithm [16]  Detailed description of operations on SMG can be found at [16].Here we provide a brief overview.

Read/write data reinterpretation
This operation emulates memory modification with validity checks.
Modifications: A level of details for a memory model allows to take into account such low level interpretation as unions and provide facility for reinterpretation values even on the same offset with different types.
Algorithm supports partial values overwrite if memory for corresponding field intersects.For example: After line 5 union u will contain integer value 10 with size 4 byte, but after line 6 from this union we are able to read 1 byte char 'A' or an undefined 4 byte integer value.
Checks: For these operations, the algorithm performs checks against null pointer dereference and read/write within object bounds.

Join of SMGs
This operation is central one for abstraction and decision whether a current memory state is covered by another one and vice versa, so the algorithm can drop one of the states.It takes as input 2 SMGs G1, G2, compares their concrete memory images and produces join status with summarization SMG G.If MI(G1) ⊈ MI(G2) and MI(G1) ⊈ MI(G2) then SMGs are semantically incomparable and their join is undefined.
Algorithm travels through pair of SMGs and tries to join nodes.It is possible if nodes have same sizes, validity, and special conditions for join with abstract lists.Abstract lists are joinable if they have same head, previous and next fields offsets, a join result will have a number of elements equal to minimum from originals.Also, a result of a join region with an abstract list become an abstract list.It is possible to insert an empty list abstraction at any correct position in a graph to increase opportunity of correct join.

Summarizing sequences of objects to list abstraction
This operation comes from the shape analysis theory.Ideas for different abstractions could be found in Sagiv work [17].SMG uses single and double linked lists as abstractions.
The algorithm discovers sequences of neighboring objects which could be considered as list entry candidates and then sequentially adds them into one abstract list and increases its size.An abstraction size is considered as number of elements necessarily present in the abstraction.

Abstract list materialization
Materialization is an operation for unfolding the abstraction to memory regions on write/read from abstracted regions.

Checking equality and inequality of values and pointers
The algorithm supports incomplete checking for equality and inequality of values and pointers.In some cases, it can fail with different point-to edges from one abstracted region.
The tool performs stack variables cleaning on function exit and checking for dangling pointers to allocated memory, which helps identify memory leak errors.Let's consider analysis of a simple example:

Bit precise model
The Linux kernel operates on structures with bit fields.We implemented bit fields in CPAchecker and switched SMG operations granularity from byte to bit precision.Also, we simulate structure alignment corresponding to GCC compiler memory usage.

Predicate extension
We implemented tracking of predicates over symbolic and concrete values stored in a memory graph.This feature allows filtering infeasible paths.On branching we perform a predicate satisfiability check to decide which branch is feasible.In addition, this method allows us to extend memory region over-read and overwrite checks for arrays using an error predicate check on a data reinterpretation operation.

On-demand memory
We consider the Linux kernel as trusted code and drivers as untrusted code in following sense: all structures provided to drivers by the kernel core are controlled by the kernel.We assume that the kernel recursively initializes all structure/union fields so drivers do not require to manage these structures.We supported the current point of view as the on-demand memory (ODM) concept within CPAchecker.

Configurable Program Analysis
The theory of SMG is implemented as Configurable Program Analysis (CPA) [18] within CPAchecker under the name SMGCPA. stop checks whether MI(G1) ⊆ MI(G2) or a state has memory issues.

Experimental results
Experiments were performed with the help of Klever static verification framework [11], that is a part of LDV project [7].Klever automatically generates environment models for each separate driver.We checked memory safety for drivers of Linux 4.11.6 and Linux 4.16.10.


Imprecise environment models (258 + 96); Automatically generated environment models could mistakenly provide wrong driver initialization and cleanup.Also, some emulated functions are imprecise for correct proof of memory safety.


Absent function (139 + 58); Current environment models do not contain functions imported from other drivers.This leads to false alarms if undefined functions are important for memory safety properties.


Require predicate SMG (83 + 43); These false alarms are connected mainly with arithmetic operations on unknown values.We expect that some common patterns used in software could be emulated by additional predicates description, e.g.bitwise AND on unsigned values provide result value less or equal to operands and this is common check for array dereference in the Linux kernel. SMG problems (13 + 32); Problems with analysis such as missed values after merge and wrong assumptions about loop invariants.


Verification task generator problems (10 + 5); The verification task generator omits information about packed pragma for structures at final source files.Sometimes it provides less allocation sizes than unpacked structure sizes.


Unknown allocation sizes (9 + 3); If SMG can not derive explicit values for allocation sizes it uses a predefined value, which may be less than required.

Conclusions and future work
We have presented the approach to find memory errors in Linux kernel drivers using static verification.Whereas the Linux kernel is widely tested, our experiments show that it is possible to find memory bugs in Linux kernel drivers with help of our static verification method.We expect to reduce the false alarm rate by introducing a more precise predicate extension.Further efforts will be aimed at reducing the number of timeouts.

Fig. 1 .
Fig. 1.Modification: allocate the 4 byte memory region on stack for pointer array

Fig. 3 .
Fig. 3. Modification: allocate the 4 byte memory region on stack for variable c and assign it a new value #2 with explicit value 3 Check: a memory region size is sufficient for the assigned value

Fig. 6 .
Fig. 6.Modification: assign 4 byte value #2 by offset 5 of region calloc_ID3, remove intersecting values, so value at offset 4 of region calloc_ID3 is not defined Check: dereference and assignment are done within allocated memory.

Fig. 7 .
Fig. 7. (a) probe function Klever provides a full error trace from an entry point to a error occurrence for the Unsafe verdict.The parts of the error trace for the Samsung I2S Controller driver are shown in fig. 7. Fig. 7 (a) shows a part of the error trace with the declaration of the variable struct i2s_dai *pri_dai in function samsung_i2s_probe().In the same function in fig.7 (b) pri_dai is initialized by function i2s_alloc_dai() (line 1246), and field sec_dai becomes NULL (line 1095).The third part of the error trace in fig.7.(c) shows that sec_dai initialization is skipped by condition in line 1319 (quirks & QUIRK_SEC_DAI) triggered by device capabilities, so pri_dai is remained equal to NULL.In the fig.7, (d) we see that the structure pri_dai becomes stored at driver_data by dev_set_drvdata() in line 1363 and then extracted by dev_get_drvdata() in line 1382 of samsung_i2s_remove(). Next the driver assigns sec_dai in line 1383 and then perform dereference of sec_dai in line 1386 without check for NULL, which leads to NULL pointer dereference.The bug can be reproduced on Samsung s3c6410-i2s and exynos7-i2s1 devices by inserting and removing driver module sound/soc/samsung/i2s.ko,because the condition in line 1319 is false for i2sv3_dai_type and i2sv5_dai_type_i2s1 (see lines 1454 and 1477 in sound/soc/samsung/i2s.c).

Table . 1
. Ratio of memory problems corresponding to all generic faults Allocation of ODM is made by special function void* ext_allocation().A returned pointer allows any recursive dereference by any offset and distinguishes values by list of offsets and pointers from the original pointer.Additionally, any explicitly allocated memory which is reachable from on-demand memory is considered as automatically freed on program exit.
o read without previous read or write:  valid for any offset;  returns nondeterministic values for non-pointer types and a pointer to ODM for pointer types; o read after write:  valid for any offset;  returns values that were written by write; o read after read:  valid for any offset;  returns the same values that were read previously;o read after free is not valid.o read or write of freed ODM is not valid.
Common CPA has abstract domain, transfer, merge and stop operators:  abstract domain describes abstract states which represent sets of concrete states of the program;  transfer gets one state and a control flow operation as input and returns all states which appears after applying the operation on the original state;  merge takes 2 states as input and tries to combine them into one;  stop identifies when one state is covered by others and decides whether it is required to continue analysis with a current state.CPAchecker allows to combine different CPAs into one composite CPA.It works with a composite state which includes states of each involved CPAs.Merge produces a Cartesian product of separate analyses merge results.SMGCPA fits into CPA conception with the following operators:  abstract domain has SMG states as abstractions;  transfer performs SMG transformations corresponding to a current control flow operation;  merge tries to join SMGs from states and returns new SMG if join is successful;

Table 2
and 3 present results of experiments on 6224 and 5215 generated verification tasks for Linux 4.11.6 and 4.16.10 respectively.We used the 15 minutes CPU time limit for each verification task.We performed manual analysis of 561 Unsafe verdicts for Linux 4.11.6 and 266 Unsafe verdicts for Linux 4.16.10 and classified 49 Unsafes as real memory bugs and 512 as false alarms for Linux 4.11.6 and 29 real bugs and 237 false alarms for Linux 4.16.10.

Table 2 .
Evaluation on drivers of Linux 4.11.6