triangulatedexistence

I uncovered an ACPI bug in my Dell Inspiron 5567. It was plaguing me for 8 years.

Imagine you close your laptop lid to put it to sleep, but instead of pausing, it reboots. Not every time, just often enough to be infuriating. You try to save your work, but the machine decides to start over.

For eight years, this has been the reality of using my Dell Inspiron 5567. A bug I couldn't explain, happening across every OS I installed. This is the story of how I dug into the firmware's source code and found the single, flawed command responsible.

Intro

This laptop has been my companion since I was in 7th grade. It's the machine where I learned everything from C++ to Python. When it couldn't upgrade to Windows 11, I gave it a new life with Linux Mint. While that came with its own set of technical puzzles to solve, one bug has been a constant frustration across every OS: S3 Sleep.

The Bug

Whenever I put my laptop to sleep, it was a gamble. Sometimes, instead of pausing, it would completely restart. This happened whether I closed the lid or let it idle.

Since the bug persisted across both Windows and Linux, I knew the fault wasn't in the operating system, but something much deeper: the firmware itself.

Ignition of spark

https://github.com/Zephkek/Asus-ROG-Aml-Deep-Dive

Literally this GitHub repo. Just check this out, it's the thing I needed. I knew that it was an ACPI fault, but I needed to know how to read the code from the ACPI tables.

In Linux (and even in Windows), this boils down to these two commands:

# Extract all ACPI tables into binary .dat files; sudo for admin privileges in Linux
sudo acpidump -b

# Decompile the main table into human-readable ACPI Source Language (.dsl)
iasl -d *.dat

This is it.

Raw code around the main problem point

I found "Method(_PTS" under dsdt.dsl. In fact, everything is under dsdt.dsl.

Note:

  • I've taken all the function calls properly and ensured that I can show you the entire process.

  • I haven't shown the scopes; I have just shown the methods.

  • The indentation has also been preserved to distinguish one method from the other.

    Method (_PTS, 1, NotSerialized)  // _PTS: Prepare To Sleep
    {
        If (Arg0)
        {
            PTS (Arg0)
            \_SB.TPM.TPTS (Arg0)
            \_SB.PCI0.LPCB.SPTS (Arg0)
            \_SB.PCI0.NPTS (Arg0)
            RPTS (Arg0)
        }
    }
    Method (PTS, 1, NotSerialized)
    {
    }
        Method (TPTS, 1, Serialized)
        {
            Switch (ToInteger (Arg0))
            {
                Case (0x04)
                {
                    RQST = Zero
                    FLAG = 0x09
                    SRSP = Zero
                    SMI = OFST /* \OFST */
                    Return (SRSP) /* \_SB_.TPM_.SRSP */
                }
                Case (0x05)
                {
                    RQST = Zero
                    FLAG = 0x09
                    SRSP = Zero
                    SMI = OFST /* \OFST */
                    Return (SRSP) /* \_SB_.TPM_.SRSP */
                }

            }
        }
                Method (SPTS, 1, NotSerialized)
                {
                    SLPX = One
                    SLPE = One
                    If ((Arg0 == 0x03))
                    {
                        AES3 = One
                    }
                }
                Method (NPTS, 1, NotSerialized)
                {
                    PA0H = PM0H /* \_SB_.PCI0.PM0H */
                    PALK = PMLK /* \_SB_.PCI0.PMLK */
                    PA1H = PM1H /* \_SB_.PCI0.PM1H */
                    PA1L = PM1L /* \_SB_.PCI0.PM1L */
                    PA2H = PM2H /* \_SB_.PCI0.PM2H */
                    PA2L = PM2L /* \_SB_.PCI0.PM2L */
                    PA3H = PM3H /* \_SB_.PCI0.PM3H */
                    PA3L = PM3L /* \_SB_.PCI0.PM3L */
                    PA4H = PM4H /* \_SB_.PCI0.PM4H */
                    PA4L = PM4L /* \_SB_.PCI0.PM4L */
                    PA5H = PM5H /* \_SB_.PCI0.PM5H */
                    PA5L = PM5L /* \_SB_.PCI0.PM5L */
                    PA6H = PM6H /* \_SB_.PCI0.PM6H */
                    PA6L = PM6L /* \_SB_.PCI0.PM6L */
                }
    Method (RPTS, 1, NotSerialized)
    {
        P80D = Zero
        D8XH (Zero, Arg0)
        ADBG (Concatenate ("_PTS=", ToHexString (Arg0)))
        If ((Arg0 == 0x03))
        {
            If (CondRefOf (\_PR.DTSE))
            {
                If ((\_PR.DTSE && (TCNT > One)))
                {
                    TRAP (0x02, 0x1E)
                }
            }
        }

        If ((IVCM == One))
        {
            \_SB.SGOV (0x02040000, Zero)
            \_SB.SGOV (0x02010002, Zero)
        }

        If (CondRefOf (\_SB.TPM.PTS))
        {
            \_SB.TPM.PTS (Arg0)
        }

        EV1 (Arg0, Zero)
    }

Explaining the problem in this raw code

After decompiling the tables, I began to trace the _PTS (Prepare To Sleep) method. It acts as a simple dispatcher, calling a sequence of other methods to prepare different hardware components.

Most of these were dead ends: the local PTS method was completely empty, and the methods for the Northbridge (NPTS) and Root Ports (RPTS) were just performing standard state-saving and debug routines.

The logic for the TPM was more interesting, but it only contained specific instructions for hibernate (S4) and shutdown (S5), doing nothing for S3 sleep. None of these were the culprit.

The main problem is about to show up, the Southbridge:

Method (SPTS, 1, NotSerialized)
{
    SLPX = One
    SLPE = One
    If ((Arg0 == 0x03))
    {
        AES3 = One
    }
}

No, not this one. I'll show you a pseudocode:

/*
================================================================================
 Southbridge_PrepareToSleep: The Buggy Method
 
 This function is called to give the final "go to sleep" command to the
 motherboard's main power controller, which lives in the Southbridge.
================================================================================
*/
void Southbridge_PrepareToSleep(int sleep_state) {
    // THE CORE LOGICAL ERROR:
    // This function needs to perform two steps in order:
    //   1. Set the hardware's "sleep_type_register" to tell it if we want
    //      S3 (pause/suspend) or S5 (stop/shutdown).
    //   2. Set the "sleep_enable_bit" to tell the hardware to "GO NOW".
    //
    // This code completely skips Step 1.

    // ----------------- THE ACTUAL BUGGY CODE -----------------

    // This line sets a secondary, auxiliary flag. It is NOT the main command
    // that tells the hardware which sleep state to enter.
    SOUTHBRIDGE.some_sleep_flag = 1;         // Original ASL: SLPX = One

    // THIS IS STEP 2 - THE "GO" BUTTON.
    // The code triggers the sleep transition immediately, without having
    // set the destination (sleep type) first. This is the root of the bug.
    SOUTHBRIDGE.sleep_enable_bit = 1;        // Original ASL: SLPE = One
    
    // This 'if' block is the firmware's broken attempt to handle S3.
    // It sets another minor flag but still fails to set the main hardware
    // sleep_type_register, so the hardware never gets the primary command.
    if (sleep_state == S3_SUSPEND) {
        SOUTHBRIDGE.acpi_s3_enable_flag = 1; // Original ASL: AES3 = One
    }
}

This is the only method in the entire sequence that unconditionally writes to what is clearly the main sleep trigger register (SLPE). The other methods are all responsible for saving state or handling their own specific hardware. SPTS is the one that recklessly pushes the "Go" button for the whole system without properly setting up the "Go where?" part first.

Let me explain some more.

Assigning SLPE to One literally instructs the motherboard, "Hey buddy, I have taken care of the rest, you can shut down everything else."

You need to realise that SLPE = One is more like a return statement, except that it instructs the motherboard to shut down. In normal programming terms, don't put any sort of statements after SLPE = One, all of them will be randomly futile.

To understand the severity of this bug, we need to look at what SLPE = One actually does. The southbridge physically contains the dedicated hardware block that controls the motherboard's power rails. When you tell the computer to enter S3 sleep, the southbridge's PMC is what actually cuts power to the CPU, RAM (partially), fans, and other components. The SLPE (Sleep Enable) bit is a direct command to this specific piece of hardware.

S3 (Deep Sleep) vs S5 (Shutdown)

We know that our _PTS dispatcher method executes like this:

    Method (_PTS, 1, NotSerialized)  // _PTS: Prepare To Sleep
    {
        If (Arg0)
        {
            PTS (Arg0)
            \_SB.TPM.TPTS (Arg0)
            \_SB.PCI0.LPCB.SPTS (Arg0)
            \_SB.PCI0.NPTS (Arg0)
            RPTS (Arg0)
        }
    }

So, the flow is like this: PTS (Literally an empty method) -> TPTS (TPM) -> SPTS (Southbridge) -> NPTS (Northbridge) -> RPTS (Root Port).

Now, let's look at our SPTS code.

                Method (SPTS, 1, NotSerialized)
                {
                    SLPX = One
                    SLPE = One
                    If ((Arg0 == 0x03))
                    {
                        AES3 = One
                    }
                }

Here, S3 isn't covered. A conditional branch is executed after SLPE = One. It doesn't make sense to even use that condition after that assignment.

The question arises when you realise that this is the same code running for S5 too: how does my computer even shut down properly then?

Notice the TPTS method:

        Method (TPTS, 1, Serialized)
        {
            Switch (ToInteger (Arg0))
            {
                Case (0x04)
                {
                    RQST = Zero
                    FLAG = 0x09
                    SRSP = Zero
                    SMI = OFST /* \OFST */
                    Return (SRSP) /* \_SB_.TPM_.SRSP */
                }
                Case (0x05)
                {
                    RQST = Zero
                    FLAG = 0x09
                    SRSP = Zero
                    SMI = OFST /* \OFST */
                    Return (SRSP) /* \_SB_.TPM_.SRSP */
                }

            }
        }

In TPTS, the same statements are written for S4 (Hibernate) and S5 (Shutdown) states. After saving the RAM to the disk, hibernation occurs by shutting everything down. TPTS saves the day.

The TPTS method executes before the buggy SPTS method. As you can see in its code, TPTS has a specific Case for S5 (Shutdown), correctly preparing the hardware.

TPTS is not responsible for S3 sleep (and it doesn't even need to be responsible in this case).

Outro

Where there's garbage, there's luck. And luck also means pulling the garbage values from the register. That's exactly what happens every time I try to close the lid of my laptop... it depends on an actual garbage value.

How will I explain all this to the 13-year-old me? How many hours were lost just thinking about this...?

Damn.


Do you know how I feel?

More like XKCD 2347.

dependency.png

In the world of AI hype, we DO deserve more tech-reviewers decoding the ACPI tables and ACTUALLY telling us if the system is stable or not. That's... the only demand from this disillusioned mind.

This was intended to be a technical report, and I rest my sorry case here.