Automated Verification of RISC-V Kernel Code

Automated Verification of RISC-V Kernel Code

Automated Verification of RISC-V Kernel Code Project Report Antoine Kaufmann June 10, 2016 1 Introduction 2. yield syscall: stores current state in kernel’s state for the process, switches to next application, and re- The longer term goal for this project is to investigate au- stores next application’s state. tomated verification of an exokernel[1]/microkernel. As- suming hardware virtualization support on the CPU as 3. sbrk syscall: if possible allocate additional memory well as I/O devices, very few operations actually need to to the application else return correct error code. be mediated by the kernel [3]. And the parts that do need 4. memory isolation: after boot and every system call, to be executed as privileged kernel code perform simple all application’s memory remains isolated. operations. Most of those operations will interact closely with hardware, be it to modify CPU control registers, or We chose the RISC-V architecture for this project be- to program an IOMMU. Because of this there is only cause it provides the required functionality but as a clean very little code where a high-level language would pro- slate approach also has a relatively compact specification. vide any benefits for implementation or verification. Thus Also useful for this project in particular is that it provides we choose to verify the kernel at the machine code level simpler mechanisms for memory access control that are for this project, and for reasons of simplicity we selected suited for embedded systems. In addition to regular vir- the RISC-V instruction set architecture [4] as our target. tual memory based on page tables, RISC-V provides sim- Code paths through our kernel are generally simple con- plified segmentation where just a single base address and sisting of few conditionals and no unbounded loops. This limit are specified for the memory accessible to the appli- makes our kernel code a good candidate for automated cation. To verify the kernel at a machine code level, we verification techniques, and SMT solvers in particular. built a Z3 model for all required instructions, their encod- For this class project we will limit ourselves to a sim- ing, and also control registers influencing execution. plified kernel as follows: All applications are already as- The rest of this report is structured as follows: Section 2 sumed to be loaded into memory prior to kernel initial- presents the RISC-V CPU model. Then section 3 de- ization, and a kernel configuration describing where they scribes our kernel including its specification, implemen- are located is passed to the kernel in memory. The kernel tation, and our verification effort and results. Finally sec- only implements two system calls: yield for switching tion 4 discusses some possible future work. between processes, and sbrk for allocating and freeing memory. One important property that an operating sys- tem needs to provide to enable reasoning about end-to-end 2 RISC-V CPU Model correctness of applications running on top of it is memory isolation. If an application cannot depend on its code and In order to be able to reason about kernel code at the data remaining unchanged at run time, any verification of machine code level, we need a model that captures how it’s properties will become impossible or at least much to execute those instructions. This section presents our more challenging. For the class project we aim to prove CPU model in Z3, discusses how we validated it, and the the following properties about the kernel: model’s current limitations. RISC-V is a modular ISA, that consists of a base in- 1. Initialization: if a valid configuration is provided the struction set to which various extensions can be added, kernel will boot to user space and start executing e.g. multiplication/division, floating point, or atomic the first application. At this point the kernel’s in- memory operations. We specify the RV64-I instructions, memory state corresponds to the configuration. which are the 64 bit core instructions. These consist of 1 control flow instructions, memory operations, linear arith- def split(self , cond, then f , e l s e f , metic, and bit operations. In addition we also implement ∗ a r g s ) : parts of the RISC-V privileged architecture, that provides assert self.split c o n d i s None the necessary mechanisms, for running a kernel with mul- tiple applications, including control transfers and memory # Check i f we know which branch to protection. Our model currently only implements a sub- t a k e set of the privileged architecture. In particular we only cc, sc = check cond(cond, self.s) implement the machine and user privilege levels and sim- s = s e l f . s ple base and bounds memory protection, that can restrict i f cc == True : user code to a contiguous segment of physical memory. t h e n f ( s e l f , ∗ a r g s ) This simpler memory protection scheme only requires two elif cc == False: CPU registers and does not require parsing of page tables. e l s e f ( s e l f , ∗ a r g s ) e l s e : 2.1 SMT Model t ms = s e l f . copy ( ) e ms = s e l f . copy ( ) Below we describe our SMT model that captures the full machine state and how it is modified by instruction. s e l f . s p l i t c o n d = sc s e l f . s p l i t t h e n = t ms s e l f . s p l i t e l s e = e ms 2.1.1 Symbolic Execution s e l f . mem = None For our first try we started out building a pure Z3 expres- s e l f . cpu = None sion that, given the initial machine state, captures the full process of fetching an instruction, decoding it, executing s . push ( ) it, represents the modified machine state. We then chained s . add ( sc ) those expressions together to execute multiple instruc- t h e n f ( t ms , ∗ a r g s ) tions, while using Z3’s simplify tactic to cut down the ex- s . pop ( ) pressions to only reachable parts for scenarios where most of the state was concrete. For executing simple sequences s . push ( ) of instructions without conditional branches from a fully s.add(Not(sc)) concrete state this approach worked reasonably well, be- e l s e f ( e ms , ∗ a r g s ) cause Z3 can basically just evaluate the expression. How- s . pop ( ) ever we quickly found for slightly more complicated in- structions sequences that this approach often leads to huge Figure 1: Method in our state class for generating condi- expressions because simplify is not always able to decide tional expressions. It takes a condition, the two functions conditionals in the expressions, even when Z3 itself actu- that operate on the respective branches, as well as a set of ally has sufficient information to choose a branch. If this arguments to pass to the branches. happens for the program counter, then building up the next step of instructions is then generally not able to simplify at d e f condbranch(ms, instr , cond): all, leading to a huge expression catching all possible in- pc = ms.cpu.read p c ( ) structions, even if in fact only one instruction is possible. d e f t h e n f ( ms t, instr, pc): This results in huge performance problems, and in prac- ms t.cpu.write pc(pc + instr. tice lead to Z3 no longer being able to prove even simple imm sextx ( ) ) things about such concrete sequences of instructions. d e f e l s e f ( ms f, instr, pc): Therefor we adapted our model to use a symbolic exe- ms f.cpu.write p c ( pc + 4) cution approach. So for all conditionals in the expression ms.split(cond, then f , e l s e f , we use Z3 to determine which branches are reachable, and i n s t r , pc ) then only generating separate expressions for the reach- able branches remembering the path condition for each. With this we basically build up expressions top down, Figure 2: Function that implements handling for condi- and only actually build parts of the expression that will tional jump instructions based on our state split mecha- be needed for evaluation. This basically means that we nism. 2 are now no longer operating on a single expression repre- senting the current state, but on a set of expressions cap- mem sort = ArraySort(mach b v s or t , turing possible state, and then potentially splitting indi- b y t e b v s o r t ) vidual expressions whenever a non-decidable conditional c p u s t a t e sort = Datatype(’CPUState’) occurs. Figure 1 shows the code in our state class for han- c p u s t a t e sort.declare(’cpu s t a t e ’ , dling this splitting of the state based on which branches ( ’ pc ’ , m a c h b v s o r t ) , are reachable Unfortunately this splitting of expressions (’regs’, ArraySort(regidx s o r t makes code somewhat harder to write, because separate , m a c h b v s o r t ) ) , functions for generating the individual branches need to # trap setup csrs be provided, and after splitting, the state can no longer ( ’ c s r mstatus’, mach b v s o r t ) , be directly modified, but we provide a do with method ( ’ c s r mtvec’, BitVecSort(xlen that applies a function to each child state recursively.

View Full Text

Details

  • File Type
    pdf
  • Upload Time
    -
  • Content Languages
    English
  • Upload User
    Anonymous/Not logged-in
  • File Pages
    8 Page
  • File Size
    -

Download

Channel Download Status
Express Download Enable

Copyright

We respect the copyrights and intellectual property rights of all users. All uploaded documents are either original works of the uploader or authorized works of the rightful owners.

  • Not to be reproduced or distributed without explicit permission.
  • Not used for commercial purposes outside of approved use cases.
  • Not used to infringe on the rights of the original creators.
  • If you believe any content infringes your copyright, please contact us immediately.

Support

For help with questions, suggestions, or problems, please contact us