2013-02-10

サムソンラップトップの文鎮化問題、問題の特定とWindows上での再現に成功

mjg59 | Samsung laptop bug is not Linux specific

まとめ。Windows 8適合のUEFIハードウェアには、少なくとも64KBのUEFI変数用のストレージ容量がなければならない。サムソンのラップトップで、このUEFI変数に、ある程度の量を書き込むと、文鎮化してしまう。

本日、サムソンのラップトップの文鎮化に成功した。これまでのサムソンのラップトップのブート不可の報告とは違い、私はLinuxはブートしていない。すべてWindows上で行われた。どうやら、バグはある意味予想していたよりも単純で、ある意味複雑であるといえる。

とりあえず事情説明から。当初想定されていた不具合は、サムソンのラップトップのドライバーが何かシステムを動作不能にさせているのだと推測されていた。このドライバーは、標準の方法ではアクセスできないラップトップの機能にアクセスするための、サムソン独自仕様にあわせて書かれていた。その動作は、特別なメモリ領域からサムソン独自のシグネチャを探し、もし発見できたならば、テーブルのポインターをたどって様々なマジックナンバーを取り出し、そのマジックナンバーを書き込んで、必要な変更を行うシステム・マネジメント・モードを発動させるものである。これは今時珍しいが、なにもサムソンが始めてというわけではない。問題は、このマジックシグネチャがUEFIシステムでも存在しているのだが、テーブルに含まれているデータを使おうとすると問題が引き起こされるのだ。

この問題の根本的な理由はまだわかっていない。当初、マジックナンバーを書き込むことが問題を引き起こしているのだと考えられた。そこで、サムソンのラップトップのドライバーは、UEFIシステムでは無効になるようにパッチをあてられた。残念ながら、問題は解決しなかった。単に問題を引き起こす最も簡単な方法がふさがれただけだった。問題を引き起こすのは書き込みではなく、その次に行う処理であると判明したのだ。書き込み命令を実行すると、何らかのハードウェアエラーが発生する。Linuxカーネルはエラーを補足してログを取る。昔は、このログは簡単に読めなかった。システムはすぐにフリーズして、ハードドライブへのアクセスは不可能になってしまうので、ディスクに書き出すことができなかったのだ。UEFIシステムでは、読みやすくするための処理がある。深刻なエラーが発生した場合、カーネルは直前のメッセージをUEFI変数ストレージに書き込むのだ。そして、リブートの後、ユーザースペースから読むことが出来、クラッシュの原因の把握を容易にする。

このクラッシュダンプは、UEFIストレージ容量を10Kほど使う。Microsoftは、Windows8システムでは、少なくとも64Kのストレージ容量が提供されているよう要求している。Linuxではクラッシュダンプを一つしか保持しないので、システムがもう一度クラッシュしたら、新しく容量を消費するのではなく、既存のログを上書きする。これはUEFI規格上、完全に合法な処理であり、Appleも自前のハードウェアで似たようなことをしている。残念ながら、一部のサムソンのラップトップは、variable storage spaceに十分多く書き込まれた場合、ブート不可能になってしまうのだ。この「十分多い」というのがどのくらいの量なのかは、まだ判明していない。しかし、Windowsからそこそこ書き込んだところ、問題を引き起こすことができた。サンプルコードを上げておく。それぞれ1KBのランダムなデータを36個書き込んでいる。これをWindowsの管理者権限で実行してシステムをリブートすれば、二度とリブートできなくなる。

明らかにファームウェアのバグである。UEFI変数を書き込むのは、規格上明白に許されており、OSが変数を埋めたからと行って、ファームウェアがブートを拒否するような挙動をしていい理由はない。昔、Intelのリファレンスコードに似たようなバグがあったが、去年の初め頃修正された。今のところ、安全のためには、サムソンのすべてのラップトップで、UEFIを使ってはならない。もしWindowsを使っているのなら、再インストールしなければならない。

UEFI変数へのアクセスは、Linux上ならば、efivarsカーネルモジュールをロードした上で、/sys/firmware/efi/varsディレクトリ下にアクセスすればいいようだ。

Unified Extensible Firmware Interface - ArchWiki

不自由なOSであるMicrosoft Windowsにおいては、Win32 APIのGetFirmwareEnvironmentVariableEx/SetFirmwareEnvironmentVariableExでアクセスできる。

GetFirmwareEnvironmentVariableEx function (Windows)
SetFirmwareEnvironmentVariableEx function (Windows)

ちなみに、Windows上で問題を再現するサンプルコードは、これだけ。SetFirmwareEnvironmentVariableExのサンプルコードとでもいったほうがいいような短さだ。コードの半分はSetFirmwareEnvironmentVariableEx利用に必要な権限取得のためなので、実質半分。

    #include "stdafx.h"
    #include <Windows.h>
    #include <WinBase.h>

    /* Write 48 UEFI variables of 1K each */
    /* The worst case outcome of this should be an error when the firmware runs out of space */
    /* However, if run on some Samsung laptops, this will cause the firmware to fail to initialise and prevent the system from ever booting */
    int _tmain(int argc, _TCHAR* argv[])
    {
            char testdata[1024];
            char name[] = "TestVarXX";
            BOOL result;
            HANDLE handle = NULL;
            TOKEN_PRIVILEGES tp;
     
            ZeroMemory(&tp, sizeof(tp));
     
            if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &handle))
                    printf("Failed to open process\n");
     
            /* Writing to UEFI variables requires the SE_SYSTEM_ENVIRONMENT_NAME privilege */
            if (!LookupPrivilegeValue(NULL, SE_SYSTEM_ENVIRONMENT_NAME, &tp.Privileges[0].Luid))
                    printf("Failed to locate privilege");
     
            tp.PrivilegeCount = 1;
            tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
            if (!AdjustTokenPrivileges(handle, FALSE, &tp, 0, NULL, 0))
                    printf("Failed to adjust privileges\n");
     
            for (int i=0; i<48; i++) {
                    /* Fill variable with 1K of random data */
                    for (int j=0; j<sizeof(testdata); j++)
                            testdata[j] = (char)rand();

                    /* Generate a unique name */
                    sprintf_s(name, sizeof(name), "TestVar%d", i);

                    /* Actually write the variable - this calls the SetVariable() UEFI runtime service */     
                    result = SetFirmwareEnvironmentVariableExA(name, "{12345678-1234-1234-1234-1234567890ab}", testdata, sizeof(testdata), 0x07);
     
                    if (!result) {
                            printf("Received error code %ld\n", GetLastError());
                            break;
                    }
            }
     
            if (result)
                            printf("Success");
     
            return 0;
    }

No comments: