building Debian packages under qemu with sbuild
I've been using sbuild for a while to build my Debian packages,
mainly because it's what is used by the Debian autobuilders, but
also because it's pretty powerful and efficient. Configuring it just
right, however, can be a challenge. In my quick Debian development
guide, I had a few pointers on how to
configure sbuild with the normal schroot
setup, but today I finished
a qemu based configuration.
Why
I want to use qemu mainly because it provides better isolation than a chroot. I sponsor packages sometimes and while I typically audit the source code before building, it still feels like the extra protection shouldn't hurt.
I also like the idea of unifying my existing virtual machine setup with my build setup. My current VM is kind of all over the place: libvirt, vagrant, GNOME Boxes, etc?). I've been slowly converging over libvirt however, and most solutions I use right now rely on qemu under the hood, certainly not chroots...
I could also have decided to go with containers like LXC, LXD, Docker
(with conbuilder, whalebuilder, docker-buildpackage),
systemd-nspawn (with debspawn), unshare (with schroot
--chroot-mode=unshare
), or whatever: I didn't feel those offer the
level of isolation that is provided by qemu.
The main downside of this approach is that it is (obviously) slower than native builds. But on modern hardware, that cost should be minimal.
How
Basically, you need this:
sudo mkdir -p /srv/sbuild/qemu/
sudo apt install sbuild-qemu
sudo sbuild-qemu-create -o /srv/sbuild/qemu/unstable-autopkgtest-amd64.img unstable https://deb.debian.org/debian
Then to make this used by default, add this to ~/.sbuildrc
:
# run autopkgtest inside the schroot
$run_autopkgtest = 1;
# tell sbuild to use autopkgtest as a chroot
$chroot_mode = 'autopkgtest';
# tell autopkgtest to use qemu
$autopkgtest_virt_server = 'qemu';
# tell autopkgtest-virt-qemu the path to the image
# use --debug there to show what autopkgtest is doing
$autopkgtest_virt_server_options = [ '--', '/srv/sbuild/qemu/%r-autopkgtest-%a.img' ];
# tell plain autopkgtest to use qemu, and the right image
$autopkgtest_opts = [ '--', 'qemu', '/srv/sbuild/qemu/%r-autopkgtest-%a.img' ];
# no need to cleanup the chroot after build, we run in a completely clean VM
$purge_build_deps = 'never';
# no need for sudo
$autopkgtest_root_args = '';
Note that the above will use the default autopkgtest (1GB, one core) and qemu (128MB, one core) configuration, which might be a little low on resources. You probably want to be explicit about this, with something like this:
# extra parameters to pass to qemu
# --enable-kvm is not necessary, detected on the fly by autopkgtest
my @_qemu_options = ('--ram-size=4096', '--cpus=2');
# tell autopkgtest-virt-qemu the path to the image
# use --debug there to show what autopkgtest is doing
$autopkgtest_virt_server_options = [ @_qemu_options, '--', '/srv/sbuild/qemu/%r-autopkgtest-%a.img' ];
$autopkgtest_opts = [ '--', 'qemu', @_qemu_options, '/srv/sbuild/qemu/%r-autopkgtest-%a.img'];
This configuration will:
- create a virtual machine image in
/srv/sbuild/qemu
forunstable
- tell
sbuild
to use that image to create a temporary VM to build the packages - tell
sbuild
to runautopkgtest
(which should really be default) - tell
autopkgtest
to useqemu
for builds and for tests
Note that the VM created by sbuild-qemu-create
have an unlocked root
account with an empty password.
Other useful tasks
Note that some of the commands below (namely the ones depending on
sbuild-qemu-boot
) assume you are running Debian 12 (bookworm) or
later.
enter the VM to make test, changes will be discarded (thanks Nick Brown for the
sbuild-qemu-boot
tip!):sbuild-qemu-boot /srv/sbuild/qemu/unstable-autopkgtest-amd64.img
That program is shipped only with bookworm and later, an equivalent command is:
qemu-system-x86_64 -snapshot -enable-kvm -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0,id=rng-device0 -m 2048 -nographic /srv/sbuild/qemu/unstable-amd64.img
The key argument here is
-snapshot
.enter the VM to make permanent changes, which will not be discarded:
sudo sbuild-qemu-boot --read-write /srv/sbuild/qemu/unstable-autopkgtest-amd64.img
Equivalent command:
sudo qemu-system-x86_64 -enable-kvm -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0,id=rng-device0 -m 2048 -nographic /srv/sbuild/qemu/unstable-autopkgtest-amd64.img
update the VM (thanks lavamind):
sudo sbuild-qemu-update /srv/sbuild/qemu/unstable-autopkgtest-amd64.img
build in a specific VM regardless of the suite specified in the changelog (e.g.
UNRELEASED
,bookworm-backports
,bookworm-security
, etc):sbuild --autopkgtest-virt-server-opts="-- qemu /var/lib/sbuild/qemu/bookworm-autopkgtest-amd64.img"
Note that you'd also need to pass
--autopkgtest-opts
if you wantautopkgtest
to run in the correct VM as well:sbuild --autopkgtest-opts="-- qemu /var/lib/sbuild/qemu/unstable.img" --autopkgtest-virt-server-opts="-- qemu /var/lib/sbuild/qemu/bookworm-autopkgtest-amd64.img"
You might also need parameters like
--ram-size
if you customized it above.
And yes, this is all quite complicated and could be streamlined a
little, but that's what you get when you have years of legacy and just
want to get stuff done. It seems to me autopkgtest-virt-qemu
should
have a magic flag starts a shell for you, but it doesn't look like
that's a thing. When that program starts, it just
says ok
and sits there.
Maybe because the authors consider the above to be simple enough (see also bug #911977 for a discussion of this problem).
Live access to a running test
When autopkgtest
starts a VM, it uses this funky qemu
commandline:
qemu-system-x86_64 -m 4096 -smp 2 -nographic -net nic,model=virtio -net user,hostfwd=tcp:127.0.0.1:10022-:22 -object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0,id=rng-device0 -monitor unix:/tmp/autopkgtest-qemu.w1mlh54b/monitor,server,nowait -serial unix:/tmp/autopkgtest-qemu.w1mlh54b/ttyS0,server,nowait -serial unix:/tmp/autopkgtest-qemu.w1mlh54b/ttyS1,server,nowait -virtfs local,id=autopkgtest,path=/tmp/autopkgtest-qemu.w1mlh54b/shared,security_model=none,mount_tag=autopkgtest -drive index=0,file=/tmp/autopkgtest-qemu.w1mlh54b/overlay.img,cache=unsafe,if=virtio,discard=unmap,format=qcow2 -enable-kvm -cpu kvm64,+vmx,+lahf_lm
... which is a typical qemu commandline, I'm sorry to say. That
gives us a VM with those settings (paths are relative to a temporary
directory, /tmp/autopkgtest-qemu.w1mlh54b/
in the above example):
- the
shared/
directory is, well, shared with the VM - port
10022
is forward to the VM's port22
, presumably for SSH, but not SSH server is started by default - the
ttyS1
andttyS2
UNIX sockets are mapped to the first two serial ports (usenc -U
to talk with those) - the
monitor
UNIX socket is a qemu control socket (see the QEMU monitor documentation, alsonc -U
)
In other words, it's possible to access the VM with:
nc -U /tmp/autopkgtest-qemu.w1mlh54b/ttyS2
The nc
socket interface is ... not great, but it works well
enough. And you can probably fire up an SSHd to get a better shell if
you feel like it.
Unification with libvirt
Those images created by autopkgtest can actually be used by libvirt to boot real, fully operational battle stations, sorry, virtual machines. But it needs some tweaking.
First, we need a snapshot image to work with, because we don't want libvirt to work directly on the pristine images created by autopkgtest:
sudo qemu-img create -f qcow2 -o backing_file=/srv/sbuild/qemu/unstable-autopkgtest-amd64.img,backing_fmt=qcow2 /var/lib/libvirt/images/unstable-autopkgtest-amd64.img 10G
sudo chown libvirt-qemu '/var/lib/libvirt/images/unstable-autopkgtest-amd64.img'
Then this VM can be adopted fairly normally in virt-manager. Note that it's possible that you can set that up through the libvirt XML as well, but I haven't quite figured it out.
One twist I found is that the "normal" networking doesn't seem to work
anymore, possibly because I messed it up with vagrant. Using the
bridge doesn't work either out of the box, but that can be fixed with
the following sysctl
changes:
net.bridge.bridge-nf-call-ip6tables=0
net.bridge.bridge-nf-call-iptables=0
net.bridge.bridge-nf-call-arptables=0
That trick was found in this good libvirt networking guide.
Finally, networking should work transparently inside the VM now. To
share files, autopkgtest expects a 9p filesystem called
sbuild-qemu
. It might be difficult to get it just right in
virt-manager, so here's the XML:
<filesystem type="mount" accessmode="passthrough">
<source dir="/home/anarcat/dist"/>
<target dir="sbuild-qemu"/>
<address type="pci" domain="0x0000" bus="0x07" slot="0x00" function="0x0"/>
</filesystem>
The above shares the /home/anarcat/dist
folder with the VM. Inside
the VM, it will be mounted because there's this /etc/fstab
line:
sbuild-qemu /shared 9p trans=virtio,version=9p2000.L,auto,nofail 0 0
By hand, that would be:
mount -t 9p -o trans=virtio,version=9p2000.L sbuild-qemu /shared
I probably forgot something else important here, but surely I will remember to put it back here when I do.
Note that this at least partially overlaps with hosting.
Boot time optimizations
Grub
echo 'GRUB_TIMEOUT=1' > /etc/default/grub.d/grub_timeout.cfg
update-grub
Note that this is now the default, at least from bookworm, see
/usr/share/sbuild/sbuild-qemu-create-modscript
.
systemd-networkd
In /etc/systemd/network/ether.network
:
[Match]
Type=ether
# Could also be Name=eth0 or Name=!lo
[Network]
DHCP=yes
Then to switch:
# WARNING: only on a console that will survive network shutdown!
systemctl disable --now networking.service ; \
systemctl enable --now systemd-networkd
That is not done automatically, and ifupdown is still the default in Debian as of this writing (2024-08-19).
Nitty-gritty details no one cares about
Fixing hang in sbuild cleanup
I'm having a hard time making heads or tails of this, but please bear with me.
In sbuild
+ schroot
, there's this notion that we don't really need
to cleanup after ourselves inside the schroot, as the schroot will
just be delted anyways. This behavior seems to be handled by the
internal "Session Purged" parameter.
At least in lib/Sbuild/Build.pm, we can see this:
my $is_cloned_session = (defined ($session->get('Session Purged')) &&
$session->get('Session Purged') == 1) ? 1 : 0;
[...]
if ($is_cloned_session) {
$self->log("Not cleaning session: cloned chroot in use\n");
} else {
if ($purge_build_deps) {
# Removing dependencies
$resolver->uninstall_deps();
} else {
$self->log("Not removing build depends: as requested\n");
}
}
The schroot
builder defines that parameter as:
$self->set('Session Purged', $info->{'Session Purged'});
... which is ... a little confusing to me. $info is:
my $info = $self->get('Chroots')->get_info($schroot_session);
... so I presume that depends on whether the schroot was correctly cleaned up? I stopped digging there...
ChrootUnshare.pm
is way more explicit:
$self->set('Session Purged', 1);
I wonder if we should do something like this with the autopkgtest backend. I guess people might technically use it with something else than qemu, but qemu is the typical use case of the autopkgtest backend, in my experience. Or at least certainly with things that cleanup after themselves. Right?
For some reason, before I added this line to my configuration:
$purge_build_deps = 'never';
... the "Cleanup" step would just completely hang. It was quite bizarre.
Disgression on the diversity of VM-like things
There are a lot of different virtualization solutions one can use (e.g. Xen, KVM, Docker or Virtualbox). I have also found libguestfs to be useful to operate on virtual images in various ways. Libvirt and Vagrant are also useful wrappers on top of the above systems.
There are particularly a lot of different tools which use Docker, Virtual machines or some sort of isolation stronger than chroot to build packages. Here are some of the alternatives I am aware of:
- Whalebuilder - Docker builder
- conbuilder - "container" builder
- debspawn - system-nspawn builder
- docker-buildpackage - Docker builder
- qemubuilder - qemu builder
- qemu-sbuild-utils - qemu + sbuild + autopkgtest
Take, for example, Whalebuilder, which uses Docker to build
packages instead of pbuilder
or sbuild
. Docker provides more
isolation than a simple chroot
: in whalebuilder
, packages are
built without network access and inside a virtualized
environment. Keep in mind there are limitations to Docker's security
and that pbuilder
and sbuild
do build under a different user
which will limit the security issues with building untrusted
packages.
On the upside, some of things are being fixed: whalebuilder
is now
an official Debian package (whalebuilder) and has added
the feature of passing custom arguments to dpkg-buildpackage.
None of those solutions (except the autopkgtest
/qemu
backend) are
implemented as a sbuild plugin, which would greatly reduce their
complexity.
I was previously using Qemu directly to run virtual machines, and had to create VMs by hand with various tools. This didn't work so well so I switched to using Vagrant as a de-facto standard to build development environment machines, but I'm returning to Qemu because it uses a similar backend as KVM and can be used to host longer-running virtual machines through libvirt.
The great thing now is that autopkgtest
has good support for qemu
and sbuild
has bridged the gap and can use it as a build
backend. I originally had found those bugs in that setup, but all of
them are now fixed:
- #911977: sbuild: how do we correctly guess the VM name in autopkgtest?
- #911979: sbuild: fails on chown in autopkgtest-qemu backend
- #911963: autopkgtest qemu build fails with proxy_cmd: parameter not set
- #911981: autopkgtest: qemu server warns about missing CPU features
So we have unification! It's possible to run your virtual machines and Debian builds using a single VM image backend storage, which is no small feat, in my humble opinion. See the sbuild-qemu blog post for the annoucement
Now I just need to figure out how to merge Vagrant, GNOME Boxes, and libvirt together, which should be a matter of placing images in the right place... right? See also hosting.
pbuilder vs sbuild
I was previously using pbuilder
and switched in 2017 to sbuild
.
AskUbuntu.com has a good comparative between pbuilder and sbuild
that shows they are pretty similar. The big advantage of sbuild is
that it is the tool in use on the buildds and it's written in Perl
instead of shell.
My concerns about switching were POLA (I'm used to pbuilder), the fact
that pbuilder runs as a separate user (works with sbuild as well now,
if the _apt
user is present), and setting up COW semantics in sbuild
(can't just plug cowbuilder there, need to configure overlayfs or
aufs, which was non-trivial in Debian jessie).
Ubuntu folks, again, have more documentation there. Debian also has extensive documentation, especially about how to configure overlays.
I was ultimately convinced by stapelberg's post on the topic which shows how much simpler sbuild really is...
Who
Thanks lavamind for the introduction to the sbuild-qemu
package.
Why not a container?
While the article is fascinating and useful, I find it overwhelming setting up all of this to just build a package.
You can obtain the same level of security/isolation, with 1/100 of the effort.
Am I wrong?
A "container" doesn't actually exist in the Linux kernel. It's a hodge-podge collection of haphazard security measures that are really hard to get right. Some do, most don't.
Besides, which container are you refering to? I know of
unshare
, LXC, LXD, Docker, podman... it can mean so many things that it actually loses its meaning.I find Qemu + KVM to be much cleaner, and yes, it does provide a much stronger security isolation than a container.
Thanks for your answer. In general yes, a VM is more secured as the system resources are separated, in that sense you're are right. My main point was in terms of effort. Playing with croups, namespaces, and the selinux beast is surely haphazard, but nobody does it manually. The typical use case are the CI/CD pipelines, where the developer just choose the base image to use. It's just few lines of JSON or yaml and we are done. There's no need to update the system, configure services, keep an eye on the resources, etc.
Again, how you do it is amazing and clean, but surely not a scalable solution
I personally use a mix of different solutions based on the customer needs, often it's podman or Docker, but not always.
But what effort do you see in maintaining a VM image exactly?
(I also use Docker to run GitLab CI pipelines, for the record, but that's not the topic here.)
See, that's the big lie of containers. I could also just "choose a base image" for a VM, say from Vagrant boxes or whatever. I don't do that, because I like to know where the heck my stuff comes from. I pull it from official Debian mirrors, so I know exactly what's in there.
When you pull from Docker hub, you have a somewhat dubious trace of where those images come from. We (Debian members) maintain a few official images, but I find their built process to be personnally quite convoluted and confusing.
See the funny thing right there is I don't even know what you're talking about here, and I've been deploying containers for years. Are you refering to a Docker compose YAML file? Or a Kubernetes manifest? Or the container image metadata? Are you actually writing that by hand!?
That doesn't show me how you setup security in your countainers, nor the guarantees it offers. Do you run the containers as root? Do you enable user namespaces? selinux or apparmor? what kind of seccomp profile will that build need?
Those are all questions I'd need to have answered if I'd want any sort of isolation even remotely close to what a VM offers.
You still need to update the image. I don't need to configure services or keep an eye on resources in my model either.
It seems you're trying to sell me on the idea that containers are great and scale better than VMs for the general case, in an article where I specifically advise users to use VM for specific case of providing stronger isolation for untrusted builds. I don't need to scale those builds to thousands of builds a day (but i will note that the Debian buildd's have been doing this for a long time without containers).
Same, it's not one size fits all. The topic here is building Debian packages, and I find qemu to be a great fit.
I dislike containers, but it's sometimes the best tool for the job. Just not in this case.
Containers can have a good level of security/isolation, but VM isolation is still stronger.
In any case, two clear advantages that VMs have over containers are that (1) one can test entire systems, which (2) may also have a foreign architecture. There are also a number of other minor advantages to using QEMU of course, e.g. snapshotting.
For example, I maintain the keyutils package, and in the process have discovered architecture-specific bugs in the kernel, for architectures I don't have physical access to. I needed to run custom kernels to debug these, and I can't to that with containers or on porterboxes.
As a co-maintainer of scikit-learn, I've also discovered a number of upstream issues in scikit-learn and numpy for the architectures that upstreams can't/don't really test in CI (e.g. 32-bit ARM). I've run into all kinds of issues with porterboxes (e.g.: not enough space), which I don't have with a local image.
So in my case, there's no way around sbuild-qemu and autopkgtest-virt-qemu anyway. And (echoing anarcat here) on the plus side: KVM + QEMU just feel much cleaner for isolation.
If host=guest arch and KVM is enabled, the emulation overhead is negligible, and I guess the boot process could be sped up with a trick or two. Most of the time is spent in the BIOS resp. UEFI environment.
Do say more about this! I would love to get faster bootup, that's the main pain point right now. It does feel like runtime performance impact is negligible (but I'd love to improve on that too), but startup time definitely feels slow.
Are you familiar with Qemu's microvm platform? How would we experiment with stuff like that in the sbuild-qemu context? How do I turn on
host=guest
?Thanks for the feedback!
Well, on my local amd64 system, a full boot to a console takes about 9s, 5-6s of which are spent in GRUB, loading the kernel and initramfs, and so on.
If one doesn't need the GRUB menu, I guess 1s could be shaved off by setting GRUB_TIMEOUT=0 in /usr/share/sbuild/sbuild-qemu-create-modscript, then rebuilding a VM.
It seems that the most time-consuming step is loading the initramfs and the initial boot, and I while I haven't looked into it yet, I feel like this could also be optimized. Minimal initramfs, minimal hardware, etc.
I've stumbled over it about a year ago, but didn't get it to run -- I think I had an older QEMU environment. With 1.7 now in bullseye-backports, I need to give it a try again soon.
However, as far as I understand it, microvm only works for a very limited x86_64 environment. In other words, this would provide only the isolation features of a VM, but not point (1) and (2) of my earlier comment.
Not that I'm against that, on the contrary, I'd still like to add that as a "fast" option.
firecracker-vm (Rust, Apache 2.0, maintained by Amazon on GitHub) also provides a microvm-like solution. Haven't tried it yet, though.
That should happen automatically through autopkgtest. sbuild-qemu calls sbuild, sbuild bridges with autopkgtest-virt-qemu, autopkgtest-virt-qemu has the host=guest detection built in.
It's odd that there's nothing in the logs indicating whether this is happening (not even with --verbose or --debug), but a simple test is: if the build is dog-slow, as in 10-15x slower than native , it's without KVM
Note that in order to use KVM, the building user must be in the 'kvm' group.