ACPI virtual battery for QEMU KVM

I have an partial implementation of the virtual battery, for QEMU. So far I have the ability to present changed battery states whilst the guest is running but is limited to the rate that the guest polls the battery for it to notice, for operating system like microsoft windows 10 and google chromeos this is about every 2 minutes from testing. Possible future work is to get interruption working so the guest notices immediatetly, although it is not known if this is possible only with custom SSDT or requires changes in the DSDT.

It is in a 3 part as suggested, and the 3 parts are:

The idea is to simplify the bit that needs to go into QEMU to reduce the risk of adding of security issues to it, especially if this functionality is unused, and allow future development of the helper and tables without depending on more changes to QEMU.

The driver just passes ioport reads and writes to the helper for a user selected port range, and allows the helper to inject interrupts for any future success with battery state interruption, for the ACPI application, this is expected to be the "SCI", interrupt 9.

Unsolved problems

It is already in a mostly useful state for me, but the known working part is limited to responding to guest polls.

I have been attempting to get interrupts to work to speed up guest discovery that it should check the battery immediately, so there is extra code in the SSDT to try to get the guest to notice interrupts such as the "SCI", therefore this may even be found to require virtual battery application specific adjustments to QEMU itself, such as the DSDT, that it dynamically builds.

Implementing

Get the the qemu source code, it is probably best to build without changes first, pick a few guests in libvirt, and make them load the compiled qemu instead of the platform one, e.g by setting <emulator>path/to/https/gitlab.com/qemu-project/qemu.git/build/qemu-system-x86_64</emulator> in virsh -c qemu:///system then check guest still works with custom compiled qemu before continuing further.

Add the ioport.c into the qemu hw/char/ directory and modify hw/char/meson.build and qemu-options.hx (by copying the lines that relate debugcon.c) to make it build ioport.c in, and recompile qemu to incorporate it, and rebuild qemu.

Configure systemd to make a socket activated ioacpi helper.

/etc/systemd/system/ioacpi.socket

  1. [Unit]
  2. Description=ioacpi helper
  3. [Socket]
  4. ListenStream=%t/ioacpi
  5. Accept=yes
  6. [Install]
  7. WantedBy=sockets.target

/etc/systemd/system/ioacpi@.service

This is the basics, more lines could be added to set nice= and slice= and so on.

  1. [Unit]
  2. Description=acpi helper instance %i
  3. [Service]
  4. ExecStart=-/usr/local/sbin/ioacpi /var/local/ioacpi/ioacpi.file

Make some example battery states by pasting into hexedit. Each file is 20 octets long.

/var/local/ioacpi/ioacpi.full
Fully charged, with power adapter connected: 00 00 00 00 00 00 00 00 28 23 00 00 e0 2e 00 00 01 00 00 00
/var/local/ioacpi/ioacpi.low
Low battery but still with power connected: 01 00 00 00 50 20 00 00 02 03 00 00 10 00 00 00 01 00 00 00
/var/local/ioacpi/ioacpi.critical
Critical battery without power: 05 00 00 00 50 20 55 00 01 00 00 00 01 00 00 00 00 00 00 00

Copy the ioacpi.full to ioacpi.file to begin with so that the guests see a fully charged battery, to begin with

Use iasl to compile portbatt.asl to portbatt.aml, it is also useful to run iasl on portbatt.aml which decompiles to give a portbatt.dsl file, which is like the original file except neatened up and without all the comments.

Configure guests that have the <emulator/> line customised above to now use the ioport driver.

  1. <qemu:commandline>
  2. <qemu:arg value='-acpitable'/>
  3. <qemu:arg value='file=/var/lib/libvirt/images/virtbatt/portbatt.aml'/>
  4. <qemu:arg value='-chardev'/>
  5. <qemu:arg value='socket,id=ioacpi,path=/run/ioacpi,reconnect=1'/>
  6. <qemu:arg value='-device'/>
  7. <qemu:arg value='isa-ioport,iobase=0xff00,iolength=20,chardev=ioacpi'/>
  8. </qemu:commandline>

I'm using the /dev/port range beginning ff00h as that was easiest to find in hexedit, naturally that can be adjusted as long as it matches in the SSDT file.

This configuration has hw/char/ioport attempt to re-connect to the ioacpi helper once a second if necessary. Whilst the connection is off, port reads return FFh and writes are discarded, like if the hardware was pulled out. This allows qemu to recover somewhat from the situation that the helper crashes, however if the helper just freezes or SIGSTOP then the connected guest will block on access to the managed ioports, until it runs again. This is necessary to support emulation of more real ACPI hardware like the 62h 66h "thing" mentioned on the acpica site.

It may be best to try this first with a livecd with easy access to hexedit, such as clonezilla. On the host, monitor the helper output, which shows the guest polling the battery ioport addresses, such as via journalctl -f -u ioacpi@* and in the guest point hexedit at /dev/port

When hexedit in the guest scrolls to ff00h, ioacpi helper on the host reports the values being accessed and the widths.

Then test values via the guest acpi stack, such as cat /sys/bus/acpi/devices/*/power_supply/*/*

Finally, when started up with a full os, that detects the battery, the polling could be monitored in the helper output, and observe the guest considers initially there to be a fully charged battery and an online power supply.

At some point, whilst the journalctl -f -u ioacpi@* output and the guest console are in easy reach, substitute cat ioacpi.full > ioacpi.file with cat ioacpi.critical > ioacpi.file and watch the polling to see when the guest learns that the battery has just gone critical and the external power has gone, in many cases to enjoy watching what guest operating system does next usually without risk of hardware actually cutting the power soon to avoid undefined hardware behaviour, usually a very rapid no questions asked emergency shutdown.

Rationale

The main usecase motivating this is a desktop or rackmount computer on an external ups, hosting virtual guests.

Of course a portable computer with an integated battery benefits in the same way if doing virtualisation.

The external power goes off, it there is a generator it did not start or maybe that is low on fuel too, and the UPS battery eventually runs low, so it is crying out that the host shutdown soon, but the host wants the guests to have a good chance at a clean shutdown, it is in a horrid dilemma.

The guests may not react to an ACPI power button poking as an unconditional shutdown demand, and a guest agent that is not part of the kernel, might crash, so the host is left with poking ACPI, or kill the guest.

With the virtual battery, the guests see through the host to the UPS, so are able to stop activites sensitive to a surprise power off firstly, like defragmenting, and updates, and maybe transfer services to another host, and when the battery goes critical, shutdown and know the reason for it.

Plus use of the power button event relies on an arbitrary choice at the host, whereas with virtual battery, the real situation of increasing probability of a surprise hard power off allows more intelligence.

Virtual battery is already proving with me to be very useful to get a clean-ish shutdown when running a guest, especially google chromeos, where the console has become unusable due to some issues, and the guest does not support the acpi power switch as a demand to shutdown without asking are you sure, well the console is broke, I can't say yes, do it.

History

I have seen the virtual battery on the Internet, no code and not for kvm.

The ultimate idea is to have guests see the status of host UPS as if it is an ACPI battery.

See if I can implement it, so rip acpi tables from qemu, host, server and laptop, then disassemble them via iasl.

We obtain tables from /sys/firmware/acpi/tables generally linux lets root read them, so copy them to somewhere like /tmp/ and feed to iasl to get .dsl files (disassembled).

The most interesting of these are the DSDT (differentiated services table) and any SSDT (supplimentary services table)

Prototype ACPI materials can be built in .asl files, copying from interesting .dsl files and ACPI standards documents. iasl then compiles the .asl source code into .aml to be fed to KVM

The laptop provides tables describing power supply and laptop, cross reference with new and older acpi docs.

I found that you have one DSDT and multiple SSDT tables, and the easiest is to put the virtual battery in an SSDT, beucause the DSDT is built dynamically by QEMU.

This is seen by GNU/Linux upon each boot, a Windows 10 guest required a Reset this PC action to make it re-read the ACPI tables. It is not unexpected as the ACPI tables are not normally expected to change, and a full implementation reads and writes io-ports to obtain battery status from the hypervisor.

Start out with a static battery in various states of charge.

Change the values and recompile to see scenarios like fully charged and nearly empty battery, notice that GNOME does nearly immediate shutdown at 2%.

Full implementation needs a few more things.

I do think it best to use the _BST example more or less verbatim out of the latest acpi specification, althoug dumping DSDT and SDST out of as many machines and hypervisors as can lay my hands on for comparison is also important. As these may be copyrighted I would not republish those.

Further work would be to have the other tables such as _BIF call the hypervisor, so that the battery can be renamed live.

Theres a few reasons.

Guests may not even bother to implement an ACPI parser, or for that matter have some bugs, and just bang on the ioports where it thinks the battery is, so make it look identical and do the real intelligence hypervisor-side, so keep the tables simple.

and, the hypervisor can be tested by getting hexedit open and pointing it at /dev/port inside the guest.

First try, writes to the ports go to userspace, reads always return 0xe9.

Maybe qemu need modifying to get it working.

It appears necessary to modify QEMU to get read and write ioport support for this, it is a big program so whilst ACPI answer work could be done inside, it would be more agile to implement as an external process so that QEMU does not need repeated recompilation to "try things". Using the debugcon driver as a basis:

  1. -chardev socket,mux=on,id=fakeacpi,path=/tmp/fakeacpi
  2. -device isa-debugcon,iobase=0x62,chardev=fakeacpi
  3. -device isa-debugcon,iobase=0x66,chardev=fakeacpi