The Bareflank Hypervisor is an open source, lightweight hypervisor, lead by Assured Information Security, Inc. that provides the scaffolding needed to rapidly prototype new hypervisors. To ease development, Bareflank is written in C++, and includes support for exceptions and the C++ Standard Template Library (STL) via libc++. With the C++ STL, users can leverage shared pointers, complex data structures (e.g. hash tables, maps, lists, etc…), and several other modern C++ features. Existing open source hypervisors that are written in C are difficult to modify, and spend a considerable amount of time re-writing similar functionality instead of focusing on what matters most: hypervisor technologies. Furthermore, users can leverage inheritance to extend every part of the hypervisor to provide additional functionality above and beyond what is already provided.
To this end, Bareflank's primary goal is to remain simple, and minimalistic, providing only the scaffolding needed to construct more complete/complicated hypervisors including:
The core business logic will remain in the hypervisors that extend Bareflank, and not in Bareflank itself.
To support Bareflank's design approach, the entire project is licensed under the GNU Lesser General Public License v2.1 (LGPL), specifically enabling users of the project to both contribute back to the project, but also create proprietary extensions if so desired. For more information please see:
GNU Lesser General Public License, version 2.1
In addition to Bareflank’s lightweight, modular design, the entire hypervisor has been written using test driven development. As such, all of Bareflank’s code comes complete with a set of unit tests to validate that the provided code works as expected.
To execute the unit tests, run:
Bareflank uses Hippomocks for mocking C/C++ functionality in the unit tests when needed. As such, Bareflank includes a version of Hippomocks that can be used by Bareflank extensions. For more information on how to use Hippomocks, checkout some of the existing unit tests in the bfvmm, as well as the following:
Bareflank's VMM is written as a set of ELF modules. Extending the VMM is as simple as adding / replacing the existing modules with new functionality. For example, suppose you want to prototype a new memory management algorithm for your hypervisor. Doing so is as simple as replacing the memory_manager_x64 module with your own.
In most cases, your likely to be more interested in modifying the virtualization extension logic. Currently Bareflank supports Intel, but ARM and AMD are planned for future releases. The VMM is broken up into the following major components:
The support logic enables the VMM environment. For example, libc++ provides std::cout for debugging which uses the memory_manager_x64 to allocate memory, and the serial_port_intel_x64 / debug_ring to output the resulting text to. The virtualization extension logic is responsible for setting up, and enabling virtualization. This logic is specific to the architecture your using. On Intel, this consists of the VMXON, VMCS, exit handler, and intrinsics logic.
To encapsulate the architectural specific logic, each architecture has its own vCPU (for Intel this is vcpu_intel_x64) that subclasses a generic vCPU (vcpu) used by the vcpu_manager, and created by the vcpu_factory. The vcpu_factory is in its own module, specifically so that users of Bareflank can replace this module with their own factory.
The process of extending Bareflank with custom virtualization logic, starts by subclassing the code you wish to extend. In this example we will extend the exit_handler_intel_x64 to count the number of CPUIDs that have been executed, but you can subclass anything including the vmxon_intel_x64, vmcs_intel_x64, intrinsics_intel_x64 logic, etc... Worst case, you can always outright provide your own modules for this same logic. Subclassing the code that Bareflank already provides, allows you to leverage the existing scaffolding that Bareflank has, likely saving you some additional time.
In this example, when the exit handler is first created, a "count" variable is created with an initial value of 0, and when the exit handler is destroyed, the exit handler prints out the count variable using Bareflank's std::cout shortcut "bfdebug" (which also adds some additional text and color for the developer). When the default exit handler's dispatch function is called, it will call "handle_exit" each time a an instruction needs to be emulated. In this example, we overload this function and increment the count variable each time CPUID is called. Finally, we call the original handle_exit which provides the CPUID emulation for us. Note that you could also leave this last part out and emulate the CPUID instruction yourself.
The next step is to tell the VMM how to create your exit handler instead of the default one. To do this, you need to provide a new vcpu_factory. The vcpu_manager uses the vcpu_factory to create vCPUs. Thus providing a custom factory provides a means to provide custom logic here (and also provides a simple means for unit testing).
In this example, we leave all of the inputs to the vCPU as null except for our new exit handler. When the vCPU is created, the vcpu creates default objects for all arguments that are left null, and uses the arguments that are provided. Note that we use the existing vcpu_intel_x64, but you could subclass this vCPU class and provide your own custom logic here too. It's entirely up to the developer on how much you wish to reuse vs. replace.
Finally, we must provide a new list of modules to BFM when starting the hypervisor. In this example, we simply need to replace the old vcpu_factory with our new one, but you could add as many new modules as you wish here. Note that we use the BUILD_ABS macro to simplify pathing. If you use the make shortcuts, Bareflank will convert these for you. If you run bfm manually, you will need to run the build_scripts/filter_module_file.sh script to convert the macros, or use absolute / relative pathing in your module file.
Currently, Bareflank supports both in-tree and out-of-tree compilation. To use in-tree, simply place your code in a folder at Bareflank's root starting with hypervisor_* or src_* and run make. To perform out-of-tree compilation, configure your build with "-m" to point to your module file and "-e" to point to your extensions.
Bareflank Hypervisor VPID Example
Bareflank Hypervisor CPUID Count Example
Bareflank's VMM is made up of the following components:
start_vmm
stop_vmm
add_md
get_drr
local_init
local_fini
vcpu
vcpu_factory
vcpu_manager
memory_manager_x64
mem_pool bfn::unique_map_ptr_x64 page_table_entry_x64 page_table_x64 root_page_table_x64
vcpu_intel_x64
vmxon_intel_x64
exit_handler_intel_x64
vmcs_intel_x64
vmcs_intel_x64_state
vmcs_intel_x64_vmm_state vmcs_intel_x64_host_vm_state
unittest
bfn::mock_unique
bfn::mock_shared
expect_true
expect_false
expect_exception
expect_no_exception
RUN_UNITTEST_WITH_MOCKS
RUN_ALL_TESTS
debug_ring
serial_port_intel_x64
With VMWare, Bareflank will use serial0 to output bfxxx / std::cout / std::cerr by default. On some VMWare systems, the printer uses serial0, so you might have to remove (disabling is not enough) the printer prior to adding the serial device. Worst case, you can modify the .vmx file manually to setup serial0.
On physical hardware however, you might have to define the serial port during compilation to something other than the default (or if you want to use a different VMWare serial port). To tell Bareflank to use a different port, you need to define the default port prior to compiling Bareflank.
export CROSS_CXXFLAGS="-DDEFAULT_COM_PORT=0x<port #>"
By default this is set to "COM1_PORT" or "0x3f8". You can set this to any of the following:
On some Intel systems with PCI serial devices the port numbers are:
You can use the above method to define all of the parameters for serial as well. The default values are listed below, and you can change them to anything you wish:
For more information, please see serial_port_intel_x64.h