2010-12-25

... In Which It Is Revealed How An AHCI Bug Makes One's Insyde(s) Freeze

I found this code in Intel's AHCI Option ROM from the Insyde BIOS. It appears to be code to build the Translated Device Parameter Table (which is a slightly different1 implementation than the one documented in the Enhanced Working T13 Draft 1126DT).

Psuedo code  (version: Serial ATA AHCI BIOS, Version iSrc 1.20_E.0019 07092009):

Function Create TDPT for drive
    Read partition table with INT13 0x201
    If read fails, or 0xAA55 signature isn't present, goto Calculate
    Get head,sectors from FIRST Partition table entry, Ending CHS values
    heads = head+1  (because of 255 limit in partition table)
    For each partition entry
        If BOOTABLE (entry[0] == 0x80) goto UsePartition
    EndFor
    For each partition entry
        if (entry[0] == 0) and (entry[4] != 0) goto UsePartition
    EndFor


Calculate
    Call CalculateCHS - using DPT and physical size
    if no need for translation, return GOOD
    Goto CreateTDPT with cylinders, heads, sectors


UsePartition:
    Read first sector of partition with INT13 0x4200
    If the word at offset 0x1A is less than 0x100,
      and the word at offset 0x18 is less than 0x40
    then
        set heads to the byte at offset 0x1A
        set sectors to the byte at offset 0x18
    fi
    tracksize = heads * sectors
    if tracksize == 0, Goto Calculate
    DWORD size = (DPT[heads]*DPT[sectors])*DPT[cylinders]
    WORD cylinders = size / tracksize <--- Bad!
    -- if the result is greater than 65536, a divide overflow occurs
    -- which isn't handled by the BIOSes
    if (cylinders > 1024) cylinders = 1024
    if ((heads == DPT[heads]) && (sectors == DPT[sectors])) return GOOD


CreateTDPT:
    WORD at DPT[8]  = DPT[0] - Save original cylinders
    BYTE at DPT[10] = DPT[2] - Save original heads
    BYTE at DPT[7]  = DPT[3] - Save original sectors
    WORD at DPT[0]  = cylinders
    BYTE at DPT[2]  = heads
    BYTE at DPT[3]  = sectors
    BYTE at DPT[5]  = 8 if heads greater than 8, otherwise 0
    BYTE at DPT[4]  = 0xA0
    BYTE at DPT[15] = SUM( DPT[0] .. DPT[14] )

So, what goes wrong? When it breaks, it starts with bad values from the partition table, and tries to fix it with values from the boot parameter block, if it finds "valid" numbers there for heads and sectors (that is, less than or equal to 0xFF and 0x3F, respectively). When these values aren't right,  due to full disk encryption, an operating system other than Microsoft Windows, or malicious intent:
  • It uses the ending head/sector of the first partition to size the translation layer.
  • Windows 7 with 100MB partition results in unexpected values for INT13, FUNCTION=8 (eg, 0x13 heads).
  • It stores those values into the Translated Device Parameter Table..  and then some other code comes along and uses those values. While I can't find where those values are causing the exception, anything doing C/H/S translation will be unhappy.
Looking back at version Serial ATA AHCI BIOS, Version iSrc 1.20E (Gigabyte Desktop Motherboard), I found that it doesn't read from the BPB at all.  I speculate the extra read of the NTFS boot-sector was to workaround a problem on Insyde BIOS.   This version can be crashed if the two bytes in the partition table are small enough and will hang with error code 23. Award BIOS will function OK with the other unexpected values, but Insyde BIOS will still crash if it sees them.

Finally, one HP system with an Insyde BIOS has the latest(?) 'fixed' version (Serial ATA AHCI BIOS, Version iSrc 1.20_E.0024 12212009), which reads from both the partition table and the BPB, also adding  still more checks. Unfortunately, it seems as though someone messed up and added a further bug, as it doesn't actually use any of the values it reads, but rather discards them all.

New and improved UsePartition (Serial ATA AHCI BIOS, Version iSrc 1.20_E.0024 12212009):
Read first sector of partition with INT13 0x4200
if the word at offset 0x1FE is not equal 0xAA55
   and the byte at offset 0 is not equal 0xEB
   and the word at offset 0x1A is less than 0x100
then
    set heads to the byte at offset 0x1A
fi
if (tracks == 0) or ((sectors & 0x3F) == 0) Goto Calculate
-- New bug: Since sectors can be at most 0x3F from partition table
-- the newer version ALWAYS goes off to Calculate the CHS
if (sectors & 0xC0) == 0)  Goto Calculate
tracksize = heads * sectors
if tracksize == 0, Goto Calculate
DWORD size = (DPT[heads]*DPT[sectors])*DPT[cylinders]
WORD cylinders = size / tracksize <-- Uber dangerous
-- if the result is greater than 65536, a divide overflow occurs
-- which isn't handled by the BIOSes.
if (cylinders > 1024) cylinders = 1024
if ((heads == DPT[heads]) && (sectors == DPT[sectors])) return GOOD
Goto CreateTDPT

A year later and neither Acer nor Gigabyte are providing fixed BIOSes.

1Expected Final TDPT Values from a 60GB SSD:
          WORD Logical Cylinders   0x400
    BYTE Heads               0xFF
    BYTE Sectors             0x3F
    BYTE Signature           0xA0
    BYTE HeadsAbove8Flag     0x08
    BYTE Ignored             0x00
    BYTE Physical Sectors    0x3F
    WORD Physical Cylinders  0x3FFF
    BYTE Physical Heads      0x10
    BYTE Ignored[4]          0x0
    BYTE Checksum            0x89

5 comments:

  1. Can you check out this 0027 that I extracted from an Acer BIOS (filename NALG0X64.FD)? It's got an ICH9M/E device ID of 2929 but it can probably just be changed to whichever ID you need: http://www.mediafire.com/?flfa1ckwy9lcicf

    ReplyDelete
  2. Yes, that still has the 0xC0 quirk.

    Boot sector is at ES:BX
    AX contains heads,sectors from partition table

    0000:3b47 cmp word ptr es:[bx+0x1a], 0xff
    0000:3b4d ja 0x3b58

    0000:3b4f mov cx, es:[bx+0x1a]
    0000:3b53 dec cx
    0000:3b54 mov ch, ah
    0000:3b56 jmp 0x3b5a

    0000:3b58 jmp 0x3b5c

    0000:3b5a mov ax, cx ; ax = heads

    0000:3b5c or al, al
    0000:3b5e jz 0x3bb0 ; Calculate from controller

    0000:3b60 push ax
    0000:3b61 and ah, 0x3f
    0000:3b64 or ah, ah
    0000:3b66 pop ax
    0000:3b67 jz 0x3bb0


    0000:3b69 push ax
    0000:3b6a and ah, 0xc0 ; Bug!
    0000:3b6d or ah, ah
    0000:3b6f pop ax
    0000:3b70 jz 0x3bb0 ; Always Jumps to calculate

    0000:3b72 inc al
    0000:3b74 and ah, 0x3f
    0000:3b77 pus ax
    0000:3b78 mul al, ah
    0000:3b7a mov bp, ax
    0000:3b7c or ax, ax
    0000:3b7e pop ax
    0000:3b7f jnz 0x3b83

    ReplyDelete
  3. I have a GA-P55M-UD4 mobo with the F9 bios (Serial ATA AHCI BIOS, Version iSrc 1.20E).

    It would hang forever at boot time until I changed offsets 0x1c3 and 0x1c4 from 0E and 0B to FE and FF.

    ReplyDelete
  4. Thanks a lot, it solved also a mess after win 7 sp 1 upgrade / update.

    More details below, in hope to help others.

    System : dual boot windows 7 64bit / OpenSUSE 12.1,
    Gigabyte P55-UD4 mobo, latest (F9) Bios, AHCI enabled.
    Grub installed on a dedicated (sda3) primary partition (not on the MBR) which was active.
    For the sp1 upgrade I used diskpart to make the windows boot partition (sda1) active.
    The upgrade went smoothly, reboot OK.

    Then a windows update. After the update, the system was in deep halt
    (I had to wake it up with the Power On button). It looked finished, waiting for reboot.
    Then the boot stopped at stage 23.
    I had to disconnect the hard drive to be able to go into the BIOS, and change to IDE. OK.

    The MBR was bit for bit the same as before, thus it looks like windows changed something in the bios.
    (Maybe flashing the bios again would do also; didn't try).
    Anyway, altering the MBR at offsets 0x1c3 and 0x1c4 from 11 and 44 to FE and FF respectively solved my problem, as in the previous comment.
    No format / reinstall needed, again thanks a lot !

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete