I surely had a cozy life in 2025. Things went exceedingly smooth and balanced even in 2026.
In the sixth anniversary of NoirVisor (October 2024), I announced that I’ll remaster NoirVisor with the Rust programming language. After two years of long journey, it is finally here! This blog will share what I’ve learned throughout the journey. It’s not just about NoirVisor itself! You may find something else very useful for you.
NOTE: Neither does this blog serve as a recommendation or advertisement of any sort, nor does it claim Rust is better than any language. I have witnessed enough squabbles about it and I do not want myself to get involved.
What’s Beyond the Expectation?
By the time I decided to remaster it with Rust, my intention was to simply remaster the virtualization core of NoirVisor. I planned to leave the driver frameworks for Windows and UEFI as is. But as I proceed, certain things went beyond my expectation.
The #1 thing is the Cargo package manager provided by Rust. When I was still using C, my primary method to add a dependency was to stick a git submodule into my repository. It was okay at the first glance. However, pain starts at switching branches. I am not that kind of person who absolutely invokes git via command line. I use GitHub Desktop in most circumstances. If different branches contain different submodules, switching branches will cause major problems. For example, I removed dependency of Zydis and moved to iced for the disassembler engine. If I switch back to master branch, the Zydis submodule won’t come back automatically. Due to such painful experience, I decided to deprecate all submodules in NoirVisor. Luckily, it wasn’t too hard. I only had two submodules: the “zydis” and the “c99-snprintf”. The iced-x86 crate is very well known x86 disassembler. Some programmers who uses .NET would use iced-x86 to decode x86 instructions. So I replaced zydis with iced-x86. However, the iced-x86 has its own downside that it requires runtime allocation. And it took more than 2MiB of memory allocations in total as initialization. I replaced it with the yaxpeax-x86 crate, a faster disassembler which does not use allocation at all. The “c99-snprintf” was required in C for the sake of doing “printf”-style debugging. However, Rust’s innate fmt library was so useful that it is completely unnecessary to require a formatter dependency at all in Rust.
Also, many people complain that they would forget to recursively pull submodules and thereby can’t build it properly. Some may simply use the GitHub’s “Download as zip” file feature, which ignores any submodules inside. The Cargo, on the other hand, would automatically download any missing dependencies. People won’t be bothered by forgetting pulling all the submodules anymore. Cargo would handle them.
In old times, to build NoirVisor for UEFI, it is required to setup the EDK-II-Library. The setup could be pretty complex to some degree, and it only works on Windows. As I delve deeper in writing UEFI stuff in Rust, I find r-efi crate a great base library to build UEFI Runtime Driver. In this regard, I completely retired all C codes for the UEFI part. As a result, I managed to completely discard the requirement of setting up edk2 in the system. It is even possible to build NoirVisor for UEFI in Non-Windows systems such as Linux.
Because the rust-dev branch is merged into master, the EDK-II-Library repository that I once maintained is also archived as deprecated.
I also observed that it’s possible to build Windows Drivers in Linux with the help of Rust. Microsoft released the windows-sys crate compatible with #![no_std]. It contains bindings to almost all APIs from Windows.
Rust also provides its way of using Assembly. In addition to the “inline assembly”, there’re also “naked assembly” and “global assembly”.
What’s New?
The most outstanding feature is probably the newly supported AMD-Vi IOMMU. This completes the self-protection of the bluepill-style hypervisor. In the original C version, I only implemented the Intel VT-d IOMMU. One of the reason was because QEMU’s AMD-Vi IOMMU was not even stablized until version 10.0.3. And VMware doesn’t even support virtualizing AMD-Vi at all. There was no way for me to test my implementation easily on a virtual platform.
I’ve also completely untied the relationship between Intel VT-x and Intel VT-d, and also between AMD-V and AMD-Vi. NoirVisor may boot with a system that supports Intel VT-x with AMD-Vi, and also AMD-V with Intel VT-d. It may sound ridiculous but QEMU supports emulating in this way anyway.
The other point is about the memory allocation. In the original NoirVisor’s implementation, memory allocation is done via the system’s API. It is simple at the first glance. However, it renders the self-protection virtually impossible because the allocated memory can be scattered everywhere and even mixed with other kernel services. If we try to protect it with EPT/NPT, then tons of EPT-violations and #NPFs may be thrown.
In the original implementation in C, simply using the AllocatePool routine from UEFI for too many times may cause serious memory fragmentation. Windows will refuse to boot, and displays an error message saying something like “BlInitializeLibrary failed 0xC00000BB” during boot. It was seen for NoirVisor if Intel VT-d was enabled. Therefore, it becomes very necessary to reduce traces of memory allocation from system’s perspective.
As I remaster NoirVisor with Rust, I also did some research about memory allocation. And eventually I decided to port the Doug Lea’s memory allocator. Its functionality is quite complete. In order to port it to Rust, I’ve wrapped it into a Rust cargo crate. In short, you can allocate a large pool – aligned in page-granularity – from the system, then this allocator will allocate small cuts from the large pool. In this way, we simply need to protect this large, page-aligned pool with EPT/NPT and that’s enough already – no EPT-violations and #NPFs would trigger unless there really is an attacker.
You might be asking why I wasn’t adopting modern allocators such as jemalloc from Facebook, mimalloc from Microsoft and tcmalloc from Google. The reason is… they’re too big to port to NoirVisor’s usage, and I do not want to render the NoirVisor’s binary too big. In old times, Rust integrates jemalloc as the default global allocator. And as a result, even a simple hello-world would take at least 2MiB of file size. In comparison, dlmalloc is small enough. Its binary size is approximately 30KiB for x64. Plus, NoirVisor does not need to face difficulties such as high-performance concurrent allocations. The allocators mentioned above – in my opinion – is intended for high-concurrency stuff. If you don’t have bottleneck on concurrent allocations, just use a smaller allocator. Even the system allocator would suffice in that case.
In modern software engineering, CI/CD is a very common practice to automate some checks so that the PRs may guarantee a bare minimum quality. For example, PRs that can’t build or don’t pass tests will automatically found and rejected. As NoirVisor’s build process is automated by python scripts, it is now quite feasible to grant CI checks on NoirVisor.
What’s Still Unimplemented?
The NoirVisor CVM (Customizable Virtual Machine) is a feature that I would like to advertise, and stand out among other well-known bluepill hypervisors such as HyperPlatform, airhv, etc. It is an ambition of mine to make NoirVisor an open-source version of the Microsoft’s Hyper-V. However, the CVM feature was not very complete even before I remaster NoirVisor with Rust. The point is that QEMU cannot display the seabios interface at all with NoirVisor CVM as its accelerator. I would claim that making QEMU successfully display the seabios will be the milestone of NoirVisor CVM development.
Exposing the Nested Virtualization interface to the subverted host also remains unimplemented. This feature’s necessity is becoming questionable. The nested virtualization is probably better be given to the CVM guests, instead of the subverted host. Just like the Microsoft Hyper-V, Windows does not expose virtualization to the root partition, but may expose it to guest partitions. I plan to do the same, yet it is still reasonable – to some degree – to expose virtualization to subverted host.
What’s the Drastic Change?
Once upon a time, I made a plan to port NoirVisor to Linux. However, as the development of NoirVisor for UEFI advances, I have even deprecated NoirVisor for Windows. In other words, I am considering forcing NoirVisor to be booted only before the OS. The plan is to just make a kernel mode driver – which schedules the CVM guests – in the subverted host’s kernel such as Windows and Linux.
Future
Current implementation of NoirVisor of using IOMMU would mask all of its interrupts. Therefore, if IOMMU intercepts a DMA attack, NoirVisor would not know unless it actively checks the event logs. To receive IOMMU’s notification, NoirVisor will have to unmask IOMMU’s MSI and intercept physical interrupts, then filter the intercepted interrupts. That also implies APIC will have to be virtualized by NoirVisor.
In QEMU/KVM, SVM’s nested virtualization does not emulate converting INIT signals into #SX(1) exceptions. In other words, NoirVisor, in order to support booting guest OS with SMP>1, would need to fully virtualize the ICR register of Local APIC. When an INIT command is intercepted, NoirVisor will instead send a fixed interrupt to the target logical processor in order to notify it’s actually an INIT or SIPI signal.
Conclusion
It is a great honor of mine to present the remastered version of NoirVisor in Rust in the eighth anniversary of NoirVisor. Aside from NoirVisor itself, I hope you can also find something else useful to you in this blog, particularly about using Rust to benefit your development.