Skip to content

GLaDOS 1.2.27

Choose a tag to compare

@IlumCI IlumCI released this 31 Aug 14:35
· 29 commits to main since this release

The release that reads its own battery.

Reading a charge on a laptop means running the bytecode the firmware ships in its DSDT. There is no shortcut and no register to poke. So this release is mostly the interpreter that does it, and the battery is what it was built for.

The ACPI interpreter

  • Table discovery that keeps what it finds and checksums all of it. The DSDT is the one table the root list does not point at, since it hangs off the FADT at offset 40, or offset 140 on a machine whose tables sit above 4 GiB. Walking the root list never found it, which is why this kernel had never printed its address.
  • A namespace built from the AML, walked to the last byte. That assertion is the whole test: package lengths live inside the stream, so one misread length does not lose one object, it desynchronises everything after it, and a parser that is ninety per cent right produces a complete-looking namespace full of names the firmware never wrote.
  • An evaluator with the opcode set real methods contain. It runs only what a caller names, and an opcode with no arm returns an error carrying that opcode and its offset, so the next machine that needs something costs one line rather than an investigation.
  • Operation regions in system memory, system I/O and the embedded controller, with fields read at their declared bit offsets and widths.
  • src/dev/ec.rs, the embedded controller: a command port, a data port and two status bits.

The parser had to be exact and the evaluator was allowed to be partial. Those are opposite obligations, and separating them is what turned an open-ended job into a bounded one.

The walk never enters a method body. Everything that declares a name is package-delimited, so bodies are stepped over by length, and that avoids the one genuinely hard problem in AML parsing: a bare name followed by arguments is a call whose argument count depends on a declaration that may live in a table not yet loaded. ACPICA solves it with multiple passes. By the time the evaluator meets it, the namespace is complete and the arity is simply known.

Three bounds, because this is firmware bytecode running in ring 0 where a fault outside a guard is fatal. A step budget, since While (One) {} is legal AML and vendor methods contain loops that wait on hardware which may not be there. A depth cap, since a method may call itself and there is no guard page under this stack. And nothing runs unasked: building the namespace executes nothing at all, which is why a top-level Store is stepped over rather than executed even though ACPI says it should run at table load.

Region writes are off until acpi unlock, the same shape as store unlock and fat unlock. Reading a battery needs none, and a stray write to an embedded controller is not a wrong number: it is a fan that stops or a charge threshold that moves, on hardware, permanently.

The battery

battery reports charge, state, time remaining, capacity, draw and health. There is a Power page in Settings, a percentage in its own well on the taskbar, a Battery record in the language so the model can ask, and the charge and the temperature both joined the situation the machine keeps about itself.

The trap is that the unit is a field of the thing it measures. _BIF element zero says whether the whole set is milliwatts or milliamps, and machines differ, so a capacity in mAh over a rate in mW gives a number that looks like a time and is wrong by the battery's voltage. Everything converts once at the boundary. The percentage is computed before any conversion, since remaining and last-full always share a unit with each other and converting first would round twice to answer a question that needed no conversion.

0xFFFFFFFF is ACPI's "unknown" and is refused rather than believed, which is how a battery comes to report a capacity of four billion.

Three states are kept apart that are tempting to collapse: the device existing, a cell being in the bay, and the cell answering. A machine with no battery and a machine whose battery cannot be read are different, and an operator needs to know which.

Power, and turning off

  • power::tick finally has a caller. It was written to hold the governor down when the part gets hot, and nothing had ever called it, so the thermal policy had been dormant since the day it was committed.
  • The governor holds at powersave on battery and releases on mains. Each policy tracks its own hold, because both can want the governor down and only one may release it: a machine that was hot and unplugged must not return to performance the moment it cools while still on battery. Both announce themselves, since a machine that quietly halves its own clock is indistinguishable from one that is broken.
  • The nightly self-modification loop will not run on battery. It already required the small hours and no hardware input. Two passes over the corpus for a deep trial is the most expensive thing this machine does, and a laptop that spends the night improving itself into a flat battery has not improved itself.
  • Real ACPI power-off. src/cpu/mod.rs carried a comment saying that doing this by hand meant parsing the DSDT for \_S5 and writing PM1a and PM1b, an interpreter's worth of work for something the firmware already does. That was a fair trade until the interpreter existed for the battery. The firmware is still asked first; ACPI is the second chance, and "hold the button" is no longer the answer when it declines. There is no standard sleep value: \_S5 is a package the board keeps in its own namespace, which is exactly why this needed an interpreter and not a constant.

USB keyboards and mice

On the HID boot protocol, over xHCI. Enumeration, SET_PROTOCOL, an interrupt endpoint, and reports diffed against the previous one to turn state into events.

The driver took an afternoon and the bug underneath it took the rest of the day. Address Device was refused while every field this code wrote was correct: contexts aligned, slot context right, input control context reading drop 0 add 3. QEMU's own trace said what six rounds of re-reading our side could not. The first Address Device succeeds; a controller will not address a second slot to a port that already has one, and enumerate had no counterpart, so the network probe walked every port at boot looking for an adapter, kept none of them, and left every port owned by a dead slot. USB enumeration had been single-shot per boot since the day that probe was written.

A HID keyboard reports usage codes and this turns them into PS/2 scancodes for the existing decoder rather than into characters, because shift, caps lock, Alt held against Alt tapped and Alt-Tab are policy, and policy written twice disagrees with itself.

Fixed

  • Four opcodes the namespace walk could not step over, each found by printing bytes rather than reasoning about them: a region offset given as a name, one computed with Add, a bare top-level Store, and the CreateWordField family. Nodes went 70, 2491, 3367, 5110, 5889.
  • The hex dump that explains a parse failure indexed the machine's own tables while the offset came from a loaded blob, read a 575 KB offset into a 9 KB slice, and panicked. A diagnostic whose job is to explain a failure became one.
  • diag migrate sampled a single 200 ms window and called task migration broken if no second core had picked the task up inside it. It reported a failure on a boot that had just allocated a 768 MiB cache, and four consecutive runs afterwards saw the task on two, three, four and four cores.
  • A field list reuses the package-length encoding to carry a count of bits rather than a byte range. Reading it as a range makes a one-byte field report zero width.
  • The cycle bit in a transfer ring is now written last and on its own. A TRB belongs to the controller the instant that bit matches, so a single struct-sized store let it see a request whose parameter had not been written yet.

Images

Six bootable UEFI ISOs, checksums in SHA256SUMS. All boot to 23 passed, 0 failed.

Image Model Context KV cache Size
glados-1.2.27.iso Qwen3-0.6B 512 112 MiB 576 MB
glados-1.2.27-qwen35-2b.iso Qwen3.5-2B hybrid 512 12 MiB 1810 MB
glados-1.2.27-qwen35-2b-8k.iso Qwen3.5-2B hybrid 8192 192 MiB 1810 MB
glados-1.2.27-qwen35-2b-32k.iso Qwen3.5-2B hybrid 32768 768 MiB 1810 MB
glados-1.2.27-smollm2-135m.iso SmolLM2-135M 512 112 MiB 133 MB
glados-1.2.27-nomodel.iso none 33 MB

The 2B images want 5 GB of RAM, and 8 GB for the 32k. The 135M image is the one to reach for under emulation.

Known

  • The embedded controller's success path has never run. QEMU models no controller: the ports answer 0xFF, both status bits look set forever, and only the timeout is proven. Every battery figure seen under emulation is the firmware's fallback branch rather than a reading, so a percentage shown in a virtual machine is not a measurement.
  • Index fields and bank fields are refused rather than read, since both need writes and writes are off.
  • Top-level conditional blocks are skipped whole and counted. Names inside them are not defined. QEMU has one; a real laptop had 142, all _OSI checks.
  • The update key is all zeroes, so staged updates verify as NoKey and are refused until a signer is provisioned.
  • The console takes a #GP if it paints from inside an interrupt gate. Older than the fault reporter, and visible rather than silent.
  • An authored application is still left as a draft and never adopted.

Licensing

The kernel source is not licensed for reuse. The model weights are Apache-2.0 and their licence travels in each image at \GLADOS\LICENSE.TXT.