Replacing VirtualBox with QEMU

2022-10-21 - updated 2022-10-24

As part of my studies, I was recently required to use a pre-prepared VirtualBox image as a development environment. I was not very familiar using pure QEMU┬╣ without any interface such as Virtual Machine Manager┬▓ or Gnome Boxes┬│, so it was a bit of a learning process.

For anyone very new to Linux and the command line, it may be a bit hard to find your way around without any kind of graphical interface. Perhaps there are guides by other people using the tools I mentioned above that make it a bit easier for you (especially Gnome Boxes should make it a lot easier and the page linked above has good information on getting started), but the first step of converting the image should be the same.

As part of this guide, I will cover converting the VirtualBox image to a QEMU-native image, creating a snapshot or running without making changes, setting up a shared folder, clipboard sharing and USB device passthrough.

Converting the image

I got my virtual machine image in the vdi format. QEMU somewhat supports this format, however I have read accounts of issues that can arise that can be fixed by converting to the qcow2 format. The tool we need for this is "qemu-img", which on Alpine Linux can be found in the package of the same name. We will use its "convert" command, as follows:

$ qemu-img convert -f vdi -O qcow2 inputfilename.vdi outputfilename.qcow2

With the '-f' flag we specify the input format as 'vdi', with the '-O' flag we specify the output format as 'qcow2'. Replace 'inputfilename' and 'outputfilename' as appropriate for your input file and desired output file name.

Depending on your image size, this may take a while, so be patient. Once it finishes execution, you can find the resulting qcow2 file with the name you specified.

Preparing your computer

To run any virtual machine at a good speed, your computer's CPU should support virtualization technology. Most modern systems support this but it may be disabled in your BIOS settings. For Intel systems, you may find it named "Virtualization" or "VT-x", for AMD systems it may be called "AMD-V". Please consult other articles on how to enable this for your specific computer/mainboard/CPU as this differs between manufacturers. Once it is enabled, we can move on with the rest of the guide.

If your computer does not support this, performance and features may be restricted.

Installing QEMU

For running QEMU, we need a qemu-system program. This comes in many varieties for many different hardware platforms, most people will be looking for a regular x86_64 virtual machine, so you should install "qemu-system-x86_64". Again, in Alpine Linux this can be installed via a package of the same name. If you are looking for a different architecture, you know what to look for instead. :)

I recommend using virtio as a display driver in QEMU, for which we will need the "qemu-hw-display-virtio-vga" package in Alpine Linux. Not all user interfaces support the clipboard sharing, so if you want that, you can install the "qemu-ui-gtk" package for the GTK interface. For passing USB devices to the VM, you will also need the "qemu-hw-usb-host" package.

If these packages don't exist for your distribution, they may be included already or have different package names which you will have to research on your own. You can check if they are already supported with the following commands (of course you can leave out parts that you don't need):

$ qemu-system-x86_64 -vga virtio -display gtk -device qemu-xhci -device usb-host

If QEMU starts, the drivers were all found and you can quit it either by pressing 'Ctrl+a' and then 'x', or if that does not work, by pressing 'Ctrl+Alt+2' to switch to the QEMU Monitor, where you can close the VM by typing 'quit'.

If the command causes an error, you will need to look for the matching package to install whichever component is missing. If everything you need checks out, we can move on to the next part!

Starting QEMU

Pure QEMU VMs are simply started as a single command, which can get quite long with the many options we might want to add. For this reason, I put the command in a shell script to make it more easily adjustable and executable. I will call the file 'start.sh' for this guide. Please note the backslash at the end of each line, which tells the shell that it should ignore the following linebreak, which we need for every line except the last.

This is the basic script we will start with, without any fancy features:

#!/bin/sh
qemu-system-x86_64 \
	-enable-kvm \
	-m 4096 \
	-nic user,model=virtio \
	-drive file=image.qcow2,media=disk,if=virtio \
	-vga virtio -display std \
	-usb -device usb-tablet

You can see that we enable KVM for our virtual machine, which greatly speeds it up as it makes use of the CPU's and Linux Kernel's virtualization support. We allocate 4 GiB (4096 MiB) of memory for the virtual machine, you can adjust this to be more or less. We attach a NIC (network interface card) to provide network connectivity to the VM and add our previously converted image file as a hard drive to the VM. Adjust the file name as appropriate for your file. For video output, we attach the virtio graphics card and use the standard interface as a display. Finally, we attach a USB 2.0 controller and add the "usb-tablet" device, which can solve some issues with mouse control.

At this point, our script is ready to run in its most basic form. We can save it and now give our user permission to execute it:

$ chmod u+x start.sh

Now we can run it:

$ ./start.sh

If all went well, your virtual machine should now start! For your VM guest system to support features such as pausing or resizing the screen optimally, you should install the QEMU Guest Agent on the guest system. In Alpine Linux, the package is named "qemu-guest-agent".

You should shut down your virtual machine as you would shut down a regular computer and should not forcefully stop it or close the window, which may cause corruption. If your VM ever gets stuck or if you are unable to exit it for some other reason, you can press 'Ctrl+Alt+2' to switch to the QEMU Monitor and type 'quit'. If you want to switch back to the display of the VM, you can press 'Ctrl+Alt+1'.

Clipboard sharing

QEMU's GTK and Spice interfaces support clipboard sharing under both Xorg and Wayland systems. To configure this, all we need is three more lines added to our script and the Spice agent running in the guest system. In my case, the VM guest system was an old Ubuntu image, so I had to install the "spice-vdagent" package there. The package has the same name for many other Linux distributions including Alpine Linux, so simply install this in your guest system.

On the host side, we don't need to install anything (besides the GTK interface I mentioned above), we only need to extend our script by these three lines (make sure to add a backslash to the end of the previous last line):

	-chardev qemu-vdagent,id=ch1,name=vdagent,clipboard=on \
	-device virtio-serial-pci \
	-device virtserialport,chardev=ch1,id=ch1,name=com.redhat.spice.0

Now we need to modify this line from earlier to enable the GTK interface:

	-vga virtio -display gtk \

For more details on this setup, checkout this blog describing its development and implementation, which saved me in my struggle:

When you start the VM now, clipboard copy and paste should simply work!

Folder sharing

Folder sharing is another part that was a little more difficult to figure out but simple to implement. It does not require installing anything, just adding two lines of configuration to the script on the host and making a little single-line mount script on the guest.

On the host side, we add these lines to our start.sh file (again, don't forget to add a backslash to the previous end):

	-fsdev local,id=vmshare_dev,path="/home/myuser/myshare",security_model=none \
	-device virtio-9p-pci,fsdev=vmshare_dev,mount_tag=vmshare

This creates a file system device and adds it to the guest with the name "vmshare". The path should be changed to the location of your desired folder on the host which you wish to share with the guest.

Then from the guest system, we can use this script to mount this shared folder:

#!/bin/sh
sudo mount -t 9p -o trans=virtio vmshare /home/guestuser/sharedfolder -oversion=9p2000.L,posixacl,msize=104857600,cache=none

Again, replace the path "/home/guestuser/sharedfolder" with the desired location where the shared folder should be mounted on the guest system. You will need to create an empty folder for this. You may also need to replace "sudo" with "doas" depending on which program you use. Finally, make it executable:

$ chmod u+x mount.sh

Now you will be able to execute it just like the start.sh script above and it should mount your shared folder correctly. For more information, please look at the QEMU wiki article that I heavily based my setup on, as well as the documentation about the 9p file system for the many mount options:

USB Device Passthrough

Passing through a USB device is quite simple and there are a few ways to do it. Usually I just want to pass a specific device to the VM, for example a specific USB stick or a specific development board in the case of this university project. For this purpose, we need to find out its vendor- and product-id.

First, attach the device, then run the command 'lsusb', which lists all USB-related devices in your computer:

$ lsusb
Bus 001 Device 006: ID 0204:6025 Chipsbank Microelectronics Co., Ltd CBM2080 / CBM2090 Flash drive controller
Bus 001 Device 004: ID 0a5c:21e6 Broadcom Corp. BCM20702 Bluetooth 4.0 [ThinkPad]
Bus 001 Device 005: ID 04f2:b217 Chicony Electronics Co., Ltd Lenovo Integrated Camera (0.3MP)
Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
(...)

I attached a random USB stick, which shows up as the first device in this list. If you cannot find your device, perhaps run 'lsusb' once without the device attached, then once with the device attached, so that you can see the difference and figure out its internal name.

The part we are interested in is "ID 0204:0625". The first number is the vendor ID, the second number is the product ID of this device, as hexadecimal numbers. These numbers should be different for each kind of device, so if you attach an identical USB stick model by the same vendor, it may have the same vendor/product ID, but other USB sticks should be different. We can pass this device to our guest system in the VM by adding the following line to the script (again, not forgetting to add a backslash to the end of the previous last line):

	-device qemu-xhci -device usb-host,vendorid=0x0204,productid=0x0625

This attaches the virtual USB controller "qemu-xhci" and the device itself, specified by its vendor and product IDs, which you will have to adjust for your device. Note the '0x' prefix before the number, which needs to be added to tell the system that this is a hexadecimal number.

IMPORTANT NOTE: This may not work if you do not run the VM as root! If you require USB passthrough to work, run your VM with 'doas' or 'sudo' to make sure that it can capture the USB device to pass it through to the guest system. Alternatively you can add it to the script if you always require USB passthrough.

Passing a USB device while the guest system is running

You may not always want to restart your VM and edit the script to attach a device. In that case, you can use the QEMU Monitor that I previously mentioned. Use 'Ctrl+Alt+2' to switch to it and 'Ctrl+Alt+1' to switch back to the VM display.

To add a device as we did in the script, we can use the 'device_add' command, however I will also specify a name with the "id=" so that we can remove the device later:

device_add usb-host,id=myusbdevice,vendorid=0x0204,productid=0x0625

For removing the device, we would use the 'device_remove' command:

device_remove myusbdevice

Of course you can give the device a different name.

Creating a snapshot

There are many reasons to want a snapshot which can be restored later, such as corruption from improperly shutting down the VM or simply having a throwaway working environment without changing the image. We will solve this slightly differently from a traditional snapshot by creating an image that uses our original image as a backing file, which means that we only store our changes in the new file and leave the original untouched. Let's start by renaming our original image file:

$ mv image.qcow2 original.qcow2

Now we can create the the image that we will work on, that will use the original file as a backing file:

$ qemu-img create -f qcow2 -b original.qcow2 -F qcow2 temp_image.qcow2

You can see that this new "temp_image.qcow" is tiny, since it only stores the changes from the original. It only grows when you make more changes to it. If you want to revert back to the original state, all you need to do is to delete the "temp_image.qcow2" and create a new one with the above command.

We also need to adjust our script to reflect the new file name. Make sure to point the script to your "temp_image.qcow2", not the original!

	-drive file=temp_image.qcow2,media=disk,if=virtio \

Alternatively, if you never want your changes to persist at all, you can skip all the above and simply add '-snapshot' to your script. This tells QEMU to store all changes in temporary files which will be thrown away when you shut down the VM.

Summary

We have now seen how to make a VirtualBox image run with QEMU and added various features that make our life easier. The full script I use looks something like this:

#!/bin/sh
qemu-system-x86_64 \
	-enable-kvm \
	-m 4096 \
	-nic user,model=virtio \
	-drive file=temp_image.qcow2,media=disk,if=virtio \
	-vga virtio -display gtk \
	-usb -device usb-tablet \
	-chardev qemu-vdagent,id=ch1,name=vdagent,clipboard=on \
	-device virtio-serial-pci \
	-device virtserialport,chardev=ch1,id=ch1,name=com.redhat.spice.0 \
	-fsdev local,id=vmshare_dev,path="/home/myuser/myshare",security_model=none \
	-device virtio-9p-pci,fsdev=vmshare_dev,mount_tag=vmshare \
	-device qemu-xhci -device usb-host,vendorid=0x0204,productid=0x0625

Let me know how it works for you, feel free to send me an email with suggestions or questions to unicorn-spam@regrow.earth (without the '-spam').