VMCALL Service
This chapter serves as a provisional final chapter of Writing Hypervisor in Zig. While it may be expanded in the future, this will conclude the series for now. As an extra stage, this chapter implements a VMCALL service using VMCALL instruction. It is a fairly compact chapter. By utilizing the VMCALL service, the guest can make requests to the host, and conversely, the host can retrieve information from the guest. In this chapter, we will build the foundation to implement these features.
important
The source code for this chapter is in whiz-vmm-vmcall
branch.
Table of Contents
Overview of VMCALL
VMCALL is an instruction used by the guest to invoke functions of the VMM. However, although it is said to call VMM functions, this instruction itself does nothing except trigger a VM Exit. The VM Exit caused by VMCALL has a Basic Reason code of VMCALL (18)
. What happens after the VM Exit is entirely up to the implementation of the VMM.
As a trial, Ymir provides only one VMCALL service. When this VMCALL is invoked, Ymir will output a logo and a message via the serial port.
note
VMCALL is a VMX extension instruction, and if executed outside of VMX operation, it triggers a #UD: Invalid Opcode
exception. For security reasons, if you want to hide from the guest that it is being virtualized, you need to mimic this behavior by causing a #UD
exception when VMCALL is invoked. Exception injection can be done by setting the VM-Entry Interruption-Information, as covered in the Interrupt Injection chapter.
VMCALL Service
Let's define the VMCALL service. The VMCALL instruction itself does not take any arguments, so the calling convention must be defined on the VMM side. In Ymir, we adopt the convention that the service number is passed in RAX when calling VMCALL. The VMCALL service with number 0
is named hello
, which outputs a logo and a message:
const VmcallNr = enum(u64) {
hello = 0,
_,
};
pub fn handleVmcall(vcpu: *Vcpu) VmxError!void {
const rax = vcpu.guest_regs.rax;
const nr: VmcallNr = @enumFromInt(rax);
switch (nr) {
.hello => try vmcHello(vcpu),
_ => log.err("Unhandled VMCALL: nr={d}", .{rax}),
}
}
vmcHello()
is a simple function that just outputs a logo. Here, we use a logo generated by Text to ASCII Art Generator (TAAG):
const logo =
\\ ____ __ ,---. ,---..-./`) .-------.
\\ \ \ / /| \ / |\ .-.')| _ _ \
\\ \ _. / ' | , \/ , |/ `-' \| ( ' ) |
\\ _( )_ .' | |\_ /| | `-'`"`|(_ o _) /
\\ ___(_ o _)' | _( )_/ | | .---. | (_,_).' __
\\| |(_,_)' | (_ o _) | | | | | |\ \ | |
\\| `-' / | (_,_) | | | | | | \ `' /
\\ \ / | | | | | | | | \ /
\\ `-..-' '--' '--' '---' ''-' `'-'
;
fn vmcHello(_: *Vcpu) VmxError!void {
log.info("GREETINGS FROM VMX-ROOT...\n{s}\n", .{logo});
log.info("This OS is hypervisored by Ymir.\n", .{});
}
ymirsh
Finally, let's implement a userspace program that invokes VMCALL. It's somewhat ironic that the last program we write in Writing Hypervisor in Zig is a userspace application. Create a new directory called ymirsh
and write a simple program that just performs the VMCALL:
fn asmVmcall(nr: u64) void {
asm volatile (
\\movq %[nr], %%rax
\\vmcall
:
: [nr] "rax" (nr),
: "memory"
);
}
pub fn main() !void {
asmVmcall(0);
}
As decided earlier, the VMCALL service number is placed in RAX before invoking the instruction. Apart from that, nothing else is done.
Add a build configuration for ymirsh
in build.zig
. Unlike Surtr or Ymir, ymirsh
is just a userspace program, so specify .os_tag = .linux
:
const ymirsh = b.addExecutable(.{
.name = "ymirsh",
.root_source_file = b.path("ymirsh/main.zig"),
.target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .linux,
.cpu_model = .baseline,
}),
.optimize = optimize,
.linkage = .static,
});
ymirsh.root_module.addOptions("option", options);
b.installArtifact(ymirsh);
Running zig build install
will generate zig-out/bin/ymirsh
. Place this binary under /bin
inside the filesystem in rootfs.cpio.gz
, and your setup will be complete.
Summary
This completes the implementation of the VMCALL service. Finally, run the guest and the ymirsh
program to test it:
[ 0.398950] mount (43) used greatest stack depth: 13832 bytes left
[ 0.400950] ln (52) used greatest stack depth: 13824 bytes left
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Saving 256 bits of non-creditable seed for next boot
/bin/sh: can't access tty; job control turned off
~ # ./bin/ymirsh
[INFO ] vmc | GREETINGS FROM VMX-ROOT...
____ __ ,---. ,---..-./`) .-------.
\ \ / /| \ / |\ .-.')| _ _ \
\ _. / ' | , \/ , |/ `-' \| ( ' ) |
_( )_ .' | |\_ /| | `-'`"`|(_ o _) /
___(_ o _)' | _( )_/ | | .---. | (_,_).' __
| |(_,_)' | (_ o _) | | | | | |\ \ | |
| `-' / | (_,_) | | | | | | \ `' /
\ / | | | | | | | | \ /
`-..-' '--' '--' '---' ''-' `'-'
[INFO ] vmc | This OS is hypervisored by Ymir.
When ymirsh
executes the VMCALL, service number 0 (hello
) is invoked, outputting the logo and message. Although the appearance and logs are indistinguishable from Ymir's usual log output, the difference is that this output is explicitly requested by the guest.
In this chapter, we implemented a VMCALL service. The functionality we implemented was mostly meaningless since it only outputs logs, but this framework allows various interactions between the guest and the host. For example, BitVisor provides a program called dbgsh
that offers an interactive shell communicating with the VMM via VMCALL. Another use case is notifying the VMM of memory addresses to protect during boot using VMCALL, then protecting those addresses with EPT. While memory protection is basically handled by the kernel using page tables, if the kernel itself is compromised by an attacker, its security mechanisms become ineffective. By notifying the VMM of the addresses to protect just once at boot, the VMM can safeguard those specified memory areas even if the kernel is compromised. As shown, VMCALL enables a variety of possibilities depending on how you use it. Feel free to implement your own ideas.
Writing Hypervisor in Zig comes to an end. If you’ve read this far, thank you very much. Ymir, the hypervisor we’ve implemented so far, is still very much a toy. As mentioned on the top page, many features remain unimplemented. However, the fact remains that it can boot Linux. Feel free to use Ymir as a base or start completely from scratch to add your own features. I hope this fancy toy, Ymir, serves as a helpful stepping stone for you.