SystemIO-conflicten zijn geen firmware-bugs

Wat is ACPI?

De Advanced Configuration and Power Interface (ACPI)-specificatie definieert een groot aantal zaken, maar het meest interessante aspect is de hardware-abstractie die het uitvoert. Hoewel pc's nominaal een goed gedefinieerd platform zijn, is dat op hardwareniveau niet echt waar zodra je een bepaald niveau van complexiteit bereikt. Wanneer je een systeem in slaapmodus zet, wil je bijvoorbeeld de hardware in de juiste volgorde uitschakelen. Om te weten wat die volgorde is, moet je specifieke details kennen over het ontwerp van het moederbord.

In de wereld van embedded systems is de aanpak om die kennis in een bepaalde vorm in het besturingssysteem (OS) te verwerken, wat resulteert in Devicetree. ACPI kiest een andere aanpak: in plaats van die informatie te leveren als data die door OS-drivers moet worden geconsumeerd, wordt het gedistribueerd als code.

ACPI Source Language en Operation Regions

De ACPI Source Language (ASL) is een eenvoudige taal die wordt gecompileerd naar bytecode, die vervolgens tijdens runtime door het besturingssysteem wordt geïnterpreteerd. Een van de functies van deze taal is de mogelijkheid om "Operation Regions" te definiëren; dit zijn in feite structuurdefinities die de toegang tot de onderliggende hardware beschrijven.

Stel je een eenvoudig apparaat voor met twee toegankelijke registers:

  1. Een indexregister: dit bepaalt welk intern register we willen benaderen.
  2. Een dataregister: het lezen hiervan geeft de waarde van het interne register waarvan het adres momenteel in het indexregister staat, en schrijven naar dit register wijzigt dat interne register.

Een voorbeeld van een operation region-declaratie zou er zo uitzien:

OperationRegion(OPR1, SystemIO, 0x400, 0x2)
Field(OPR1, ByteAcc, NoLock, Preserve)
{
    INDX, 8
    DATA, 8
}

Dit definieert een operation region genaamd "OPR1" op IO-poort 0x400, met een lengte van 2 bytes. Hierin zitten twee 8-bit velden: INDX en DATA. Deze worden één voor één benaderd, vereisen geen globale lock van de ACPI-interpreter bij toegang, en als een subset van het register wordt gewijzigd, moeten de andere waarden behouden blijven (wat in dit geval irrelevant is aangezien de velden slechts een byte breed zijn).

Elke referentie naar INDX of DATA binnen deze scope zal nu leiden tot toegang tot die registers. Een methode om de waarde van register 0x03 te lezen, zou er dan als volgt uitzien:

Method (RD03) {
    INDX = 0x3
    Return (DATA)
}

Dit betekent: zet INDX op 3, lees vervolgens de waarde van DATA en geef deze terug.

Het risico van race-condities

Maar wat gebeurt er als er gelijktijdig een andere ACPI-methode wordt uitgevoerd? Stel dat we een methode hebben die naar register 0x05 schrijft:

Method (WR05, 1) {
    INDX = 0x05
    DATA = Arg1
}

Wat gebeurt er als RD03 wordt uitgevoerd terwijl we halverwege WR05 zijn? INDX kan worden gereset naar 0x03, waardoor WR05 register 0x03 wijzigt in plaats van register 0x05.

Om dit te voorkomen kunnen we een mutex declareren (Mutex (MUTX, 0x00)) en onze methoden als volgt bijwerken:

Method (RD03) {
    Acquire (MUTX, 0xFFFF)
    INDX = 0x3
    Local0 = DATA
    Release (MUTX)
    Return (Local0)
}

Method (WR05, 1) {
    Acquire (MUTX, 0xFFFF)
    INDX = 0x05
    DATA = Arg1
    Release (MUTX)
}

Elke methode neemt nu een lock (met een wachttijd tot 0xffff milliseconden, waarna er een fout optreedt als de lock niet verkregen is) en voert vervolgens de toegang uit. Er is nu geen kans op een race-conditie.

Conflicten met Linux-drivers

Stel nu dat iemand een Linux-driver schrijft voor dit stuk hardware. Deze driver benadert de hardware direct, zonder kennis van ACPI. Wat verhindert dat de driver in conflict komt met een van de ACPI-toegangsmethoden? Niets.

Dit is niet hypothetisch. Een relatief onschadelijk voorbeeld is dat we in het verleden gevallen tegenkwamen waarbij temperatuurmonitoring-chips gelijktijdig door de firmware en Linux werden benaderd. Als gevolg hiervan kon het gebeuren dat je dacht een temperatuur te lezen terwijl je in werkelijkheid een statusvlag las, wat leidde tot een onmogelijk hoge temperatuurwaarde en een onmiddellijke thermal shutdown.

In dit geval beschermt de kernel je tegen deze (potentieel hardware-beschadigende) uitkomst door een melding te printen zoals: ACPI Warning: SystemIO range 0x0000000000000400-0x0000000000000401 conflicts with OpRegion 0x0000000000000400-0x0000000000000401 (OPR1).

De kernel detecteert hiermee dat een driver probeert IO-poorten 0x400-0x401 toe te wijzen, maar dat er een ACPI operation region genaamd OPR1 is die dezelfde adressen claimt. De kernel kan niet weten welk type toegang de firmware in dat gebied uitvoert, gaat er dus vanuit dat dit gevaarlijk kan zijn en blokkeert het laden van de driver.

De juiste oplossing

De kernel geeft vaak ook behulpzaam advies: ACPI: If an ACPI driver is available for this device, you should use it instead of the native driver. ACPI-tabellen bevatten vaak een definitie zoals deze:

Device (HDW1)
{
    Name (_HID, "VEND0001")
    OperationRegion(OPR1, SystemIO, 0x400, 0x2)
    Field(OPR1, ByteAcc, NoLock, Preserve)
    {
        INDX, 8
        DATA, 8
    }
    Mutex (MUTX, 0)
    Method (RD03) {
        Acquire (MUTX, 0xFFFF)
        INDX = 0x3
        Local0 = DATA
        Release (MUTX)
        Return (Local0)
    }
    Method (WR05, 1) {
        Acquire (MUTX, 0xFFFF)
        INDX = 0x05
        DATA = Arg1
        Release (MUTX)
    }
}

Dit definieert een ACPI-apparaat en de bijbehorende methoden. Het _HID-veld definieert het apparaattype. Er kan een Linux-driver worden geschreven die automatisch wordt geladen als een apparaat met type VEND0001 wordt herkend. Deze driver kan vervolgens ACPI-methoden aanroepen die bij het apparaat horen en zo de resources benaderen op een manier die overeenkomt met de verwachtingen van de firmware.

Conclusie

De firmware heeft in dit scenario absoluut niets fout gedaan[^1], maar het proberen te laden van de native driver zal een fout genereren. Op internet zal men je vertellen dat pc-firmware-ontwikkelaars incompetent zijn[^2] en dat je een kernel-argument moet doorgeven dat dit gedrag overschrijft. Men zal zeggen dat dit hen nooit kwaad heeft gedaan en dat het jou waarschijnlijk ook geen kwaad zal doen, maar het zou kunnen, en je zult mogelijk nooit weten waarom je systeem af en toe vastloopt of in brand vliegt.

***

[^1]: Je zou kunnen argumenteren dat de firmware tijdens runtime simpelweg niets zou moeten doen omdat dat niet de taak van de firmware is. Ik begrijp dat standpunt en je kunt zeker booten met acpi=off als je dat wilt, waardoor er geen ACPI-code tijdens runtime wordt uitgevoerd. Laat me weten hoe dat bevalt. [^2]: Ik zal hier geen mening over geven, enkel opmerken dat dit geen bewijs levert voor die bewering. [^3]: De ACPI-specificatie was voorheen te vinden op acpi.info, maar dat lijkt ergens na de overname van de specificatie door UEFI te zijn verdwenen.