The Hunt for a UEFI Graphics Bug

2022-07-03

How much did God guide?

God said 640x480 16 color in 2004.

God said single audio voice which I assume means one frequency at a time.

God said no child windows.

I had an editor side bar. God said no.

It became clear He was guiding the work when things worked out really well.

— Terry Davis.


راح افترض انت قاري المقال السابق، The Curious Case of LenovoRuntimePassword.

قبل فترة ردت انصب OpenBSD (نظام Unix-like، مثل لينكس) على لابتوبي (Lenovo B470e) اللي يشتغل كDNS server (لحظر الاعلانات) وRADIUS server (للوايفاي).

نزلت النظام، وحطيته بفلاش، وشغلته وطلعلي هيچ:

محاكاة باستخدام QEMU

بالبداية اعتقدت انه صار crash، بس جربت ادوس بالكيبورد وشفت دتصير تغييرات على الخرابيش، فيعني المشكلة تخص الgraphics.

قبل ما طلعت هالخرابيش، طلعتلي prompt مال البوتلودر، اذا مادوس شي خلال كم ثانية راح يتلود الkernel تلقائيا.

اول شي جربت اسويه هو أتأكد من انه الimage مكتوبه للفلاش بشكل صحيح، بس طلع مابيها شي. وراها جربت اختار الفلاش يدويا عن طريق الboot menu اللي تطلع لمن ادوس F12 (گلت بلكي يفرق بشي)، وهم بقت.

جتي ببالي فكرة اعتقدت انه ماراح تجيب اي نتيجة بس گلت خالأجربها ليش لا: ربطت الmonitor مال حاسبتي (1920x1080) باللابتوب و... اشتغل طبيعي!

محاكاة باستخدام QEMU

زاد فضولي، فجربت اشغل الimage بQEMU بوضع الUEFI عن طريق هذا الcommand:

qemu-system-x86_64 -bios /usr/share/ovmf/OVMF.fd -m 500M -enable-kvm -drive file=install71.img

والاغرب انه بعد ما اختفت الprompt، صارت الresolution مال الVirtual machine اعلى من مال الmonitor مالتي بهوايه:


راح اختصر هوايه، كالعادة.

البوتلودر هو عبارة عن UEFI application، الmain function مالته شكلها كالاتي:

EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)

الSystem table هي البوابة الرئيسية اللي تگدر من خلالها تتواصل ويه الUEFI. يعني مثلا بيها pointer للBoot Services، اللي تگدر من خلالها تلعب بالprotocols عن طريق functions مثل LocateProtocol وInstallMultipleProtocolInterfaces (حچيت عنهن بالمقال الفات).

كذلك بيها pointers للConIn والConOut، اللي منهن تگدر تحصل على functions مثل OutputString وClearScreen وReadKeyStroke.

البوتلودر يسوي هوايه شغلات خلال حياته القصيرة، اهمها تحميل الkernel للذاكرة واعطائه الarguments اللي يحتاجها (مثل الSystem table والMemory map والConfiguration tables اضافة الى الframebuffer pointer وتفاصيله مثل الdimensions والsize، الخ).

قبل ما البوتلودر يشغل الkernel حيسوي call لExitBootServices، وراها بعد ماكو Boot Services: لا Handles ولا Timers ولا Events ولا Memory management ولا ConOut ولا ConIn ولا بطيخ، الkernel بعد حيحير بروحه بدون دلال الUEFI. على عكس الأهل اللي يطردون اطفالهم لمن يصير عمرهم 18 سنة، الUEFI عنده شوية حنيه وحيبقي الRuntime Drivers والRuntime Services (اللي تگدر منها تطفي/ترست الحاسبة وتتلاعب بالUEFI variables، الخ) وشكم شغله لخ.

واضح من الأمثلة الشفناها فوگ انه لازم نطلع على الresolutions ونشوف شنو سالفتها. فخلي نفصل الشاشة الخارجية ونجرب نلعب بالprompt مال البوتلودر.

منا وجاي، سكرينات البوتلودر حتكون مأخوذه من اللابتوب مباشرة عن طريق أمر machine screenshot اللي ضفته يدويا علمود هالمقال. حشرح طريقة عمله بعدين.

اكو فد شي كلش غريب بهالصورة، همممم

اكو أمرين يهمونا هنا: machine gop وmachine video. الgop مو معناه الحزب الجمهوري الأمريكي وانما Graphics Output Protocol. الأمر يستعرضلنا الmodes (يعني الresolutions) المتوفرة للframebuffer ويخلينا نغيرها اذا نكتب machine gop n (الn تمثل رقم الmode اللي نريده).

الvideo يستعرضلنا الtext modes المتوفرة للConOut، يعني 80x25 معناها هالmode ينطيك 25 سطر كل واحد يكفي 80 حرف، وهم يخلينا نغير الmodes بنفس الصيغة الفوگ.

تغيير الtext mode ممكن يغير الGOP mode وبالعكس.

خلي نجرب نكتب machine gop 2 ونشوف شيصير:

لاحظوا انه كأنه كلشي ما تغير بعد ما دخلنا الأمر. جديات، قارنوا الصورتين الفوگ وحتشوفون ماكو اي فرق بحجم الحروف. اصلا اذا انتوا فضوليين كلش، ممكن لاحظتوا انه الصورة القبل هاي الresolution مالتها 800x600، مع انه المفروض چنه بmode 0 اللي الresolution مالته 1024x768! اذا نستمر بالاقلاع، هالمرة حيشتغل النظام عادي بدون اي خرابيش.

اعتقد المشكلة صارت واضحة: النظام عباله الresolution هي 1024x768 بس بالحقيقة هي 800x600، وهالشي ديخربط كل الحسابات.

رستت وجربت اكتب machine gop 0 حتى اشوف اذا حيحول لmode 0 من صدگ، بس مصار شي، عالاكثر لأن اكو checks بالfirmware تمنع التحويل لنفس الmode. فحولت لغير mode ورجعت ل0 وهالمرة صارت 1024x768 من صدگ واشتغل بدون خرابيش.

زين لمن ربطت monitor خارجي ليش اشتغل طبيعي؟ خلي نربطه من جديد ونشوف الmodes الموجودة:

اذا المستخدم ممختار mode محدد عن طريق machine gop، البوتلودر حيختار الmode صاحب اعلى resolution، ومثل مدتشوفون بالصورة اكو mode اعلى من 1024x768 للmonitor. وهذا نفس السبب اللي يخلي الresolution تصير كلش چبيرة لمن تجرب تشغل النظام بQEMU، لأن QEMU يوفر ⁦29 mode⁩، اكبر resolution بيها هي 2560x1600، وشاشتي 1920x1080.

زين ليش هيچ ديصير؟ ليش مواجهتني هالمشكلة ويه لينكس مثلا؟ اخذت فرة بالcode، ولگيت هالfunction المثيرة للاهتمام. شغلتها تحول الtext mode الى 100x31، واذا ماكو هالmode تحول ل80x25. مثل مشفنا فوگ، الtext mode بالفعل صاير 100x31.

تتذكرون شلون قبل گلت انه تغيير الtext modes ممكن يأثر على الGOP modes وبالعكس؟ احتمال اللي ديصير هنا هو انه اللابتوب ديشتغل بmode 0 (وهالشي وارد، خصوصا انه الsplash screen اللي تطلع اول ما يشتغل دقتها عالية) وراها تغيير الtext mode ديحول الGOP mode داخليا الى mode 2 بدون ما يحَدث المعلومات مال الmode الحالي.

فحتى نأكد هالنظرية نحتاج نبني الbootloader بدون الcode مال تغيير الtext mode.


قبل ما اشتغل على الbootloader، احتاج طريقة ابنيه بيها على لينكس (للconvenience) واحتاج طريقة اجرب بيها تغييراتي بسرعة (اكيد ما راح اگعد اشيل فلاش واودي فلاش على اقل تعديل).

حتى نبنيه بOpenBSD، نحتاج ننزل sys.tar.gz ونروح لsys/arch/amd64/stand/efiboot/bootx64 ونسوي make. اذا نجرب نفس الشي بلينكس فmake راح يعيط لأن اكو اختلافات بالsyntax بين OpenBSD make و GNU make. جربت bmake (اللي هو الNetBSD make، وpackaged بDebian) بس هم گعد يصيح.

فأهنا جتي ببالي فكرة احسن: make (بأغلب الMakefiles) يكتب كل امر ينفذه، مثل هيچ:

فأخذت الoutput وسويته script. صح هالشي معناه راح يصير rebuild كل ما اشغل السكربت حتى لو ماكو اي تغيير، بس الrebuild ميطول ثانيتين بالزايد فمو هلگد مهم. حطيت set -e ببداية الscript حتى اذا صار اي خطأ يوگف الbuild، وراها استبدلت cc بclang (لأن بDebian، ‏cc هو gcc بينما بOpenBSD هو clang) وعدلت المسارات باستخدام sed (هوايه ميعرفون انه تگدر تستخدم اي delimiter يعجبك ويه sed، مو شرط /. هالشي مفيد بهالحالة حتى لتضطر تحط \ قبل اي / ضمن الpath). اخير شي استبدلت الlinker بالLLVM linker.

شغلته وطلعلي هالerror:

make شكله ما طبع فد خطوة من العملية، نگدر بسهولة نقارن قبل وبعد make ونشوف شنو الفروقات اللي بيها machine:

اها! شكله ديسوي symlink لinclude directory حسب الarchitecture.

بعد ما سويت الsymlink، اخيرا انبنه البوتلودر بداخل لينكس وصار عندي ملف BOOTX64.EFI. بقه اشد الnetwork booting حتى اگدر اجرب تغييراتي بسرعة.

نزلت iPXE وجربت اشغله، بس لاحظت انه دياخذ هوايه وكت على ما يشتغل بدون داعي (يعصى على iPXE initializing devices)، بس المهم اشتغل وگدرت انزل البوتلودر من http server على حاسبتي واشغله (عن طريق كتابة الرابط يدويا بالcommand line).

حتى اوفر وكت، ضربت عصفورين بحجر: بنيته بس ويه الethernet drivers اللي احتاجها (لأن شكيت انه ديعصى على شي يخص الوايفاي) وخليتله embedded script يخليه ينزل البوتلودر تلقائيا اول ما يشتغل:

make bin-x86_64-efi/realtek.efi EMBED=script.ipxe

محتوى script.ipxe:

#!ipxe
dhcp
chain http://link/to/BOOTX64.EFI

البوتلودر حيشتغل طبيعي وراها، بس اذا تريد تكمل الاقلاع لازم يكون عندك پارتشن بي ملف الkernel (تگدر تستخدم فلاش بي OpenBSD image خام، حيتعرف عليه عادي. او تگدر تجهز واحد يدويا) وراها مجرد گول للبوتلودر وين الkernel (مثلا boot hd1a:/bsd).


بعد ما جهزنا كلشي، خلي نجرب نمسح الcode اللي يغير الtext mode ونشغل البوتلودر:

الدقة شكلها صح والمشكلة انحلت، مو؟ خلي نجرب نشغله عن طريق الF12 boot menu:

هالمرة الboot menu شكلها دتغير الtext mode الى 0 واللي ديغير الGOP mode الى 1 داخليا هم بدون ما يحدث معلومات الcurrent mode، يعني نفس المهزلة بس هالمرة مو البوتلودر مصدرها. هسه شسوي؟

اول شي اجه ببالي هو انه احسب حجم الframebuffer من الابعاد (الطول * العرض (أو Pixels Per Scan Line) * عدد البايتات لكل بكسل (4)) واقارنه ويه الFrameBufferSize الجاهز واذا طلع اكو اختلاف اغير الmode، عسى ولعل يطلع الFrameBufferSize صحيح.

جربته بQEMU، وطلع الحجم المحسوب 1920000 بايت بينما الحجم الreported هو 1921024، صار عندي فضول اعرف شنو قصة هال1024 الاضافية، شكلها فد نوع من الalignment، فرحت ادور بالcode مال OVMF (اللي هو الUEFI firmware مال QEMU) ولگيت هالcommit. جربته باللابتوب والحجم الreported بقه ثابت (67043328 بايت) ومَتغير لو شمسويت.


Try to be like the turtle—at ease in your own shell.

— Bill Copeland.

وره ما شفت الحجم الreported بقه ثابت، واهسي بالسالفة نزل، فعفتها مؤقتا. ثاني يوم چنت بالحمام واجه ببالي الUEFI shell، اللي تگدر تتخيله مثل الDOS بنكهة الUEFI وبي هوايه سوالف تفيد الtroubleshooting، فگلت خلي اجربه.

نزلت نسخة حديثة وجربت اشغلها بس بقت الشاشة سودة، طلعت النسخ الجديدة بس تدعم UEFI 2.3 وفوگ، بينما لابتوبي الUEFI implementation مالته 2.0. نزلت نسخة اقدم منا واشتغلت.

استخدمت dh حتى اشوف رقم الhandle اللي بيها الGOP instance و... طلع اكو ثنين! هاي أول وحدة (واللي LocateProtocol تستعملها):

وهاي الثانية:

الGOP instance الثانية اكو بالhandle مالتها Device Path بينما الاولى لا، بمعنى ثاني، هي "فيزيائية" اكثر من الاولى.

جربت استخدم ثاني GOP instance بدل الاولى وانحلت كل المشاكل like a charm!

زين شلون وليش؟ سويت جلسة بحث مطولة ولگيت هالمقال مال شخص ديكتب Maze game للUEFI، ذني المقتطفات اللي تهمنا:

Find all instances of the Graphics Output protocol that are available in the system. There can be one instance per graphical device in the system. Each one of the instances can be set to a different resolution and support a different number of colors. Rather than requiring the application to manage all of the devices, most systems use the Console Splitter driver, which acts as a meta-driver, aggregating the information from all of the drivers and drawing all bitmaps on all displays. The LocateHandleBuffer() function in the UEFI Boot Services allocates a buffer to hold all of the handles that support a specified protocol

Now that we have found handles for all drivers that support the Graphics Output protocol, we examine each handle to see if it also has an instance of the Device Path protocol. Why? Because the one way to distinguish the Console Splitter from all other graphical devices in the system is that it is not actually a hardware device. Since it is not a hardware device, it does not have a Device Path protocol associated with it, since the Device Path protocol used to describe how a device is attached to the system. If we find a handle that doesn't have an instance, the pointer to that instance of the Graphics Output protocol is saved in a global variable.

شنو المشكلة بالConsole Splitter؟ ليش ملازم نستعمله؟

جلسة بحث ثانية ودتني لهلemail thread وهالcode من الEFISTUB مال لينكس. اتوقع چان اكو فترة الGOP instance مال الConsole Splitter چانت buggy ولابتوبي من ضمن هالفترة.

زين منو هذا الشحص صاحب المقال؟ صار عندي فضول فدورت على الLinkedIn مالته:

‏Phoenix Technologies هي الشركة اللي "صنعت" الfirmware مال لابتوبي.

صار عندي فضول، يا ترى سالفة التأكد من الDevice Path ديتم استخدامها بداحل الfirmware بنفسه؟ خلي نبحث عن كل مكان موجود بي الGOP GUID:

اختاريت SystemImageDisplayDxe وحطيتها بGhidra، هنا ديتم استعمال الGOP GUID بيها:

خلي نرتب شوية:

زين عرفنا انه المشاكل اللي تطلع لمن نشغل النظام باستخدام F12 مو صوچ البوتلودر، هل هذا معناه انه المشاريع المشهورة اللي تدعم UEFI هم حتواجه مشاكل لمن اشغلها بهالطريقة؟ جربت كم وحدة وهاي النتائج:

انحل اللغز، كتبت patch هم يختار اول GOP instance الhandle مالها بي Device Path instance واذا ماكو يختار اول وحدة، دزيته (وحطيت روابط كم منشور مال اشخاص عانوا نفس المشكلة ويه الايميل حتى ابين انه السالفة مو بس يمي) ب⁦2022-04-25⁩ وتم قبوله ب⁦06-20⁩.


ردت اسوي خرابيش homemade حتى اگدر احصل صورة نظيفة من داخل QEMU واحطها بهالمقال، فسويت الdimensions مال الframebuffer اللي يتم تمريرها للkernel نفسها مال mode 0 باللابتوب (1024x768)، الdefault mode مال QEMU الresolution مالته 800x600 (نفسها مال mode 2)، فمحتاجيت اغيرها.

حتى اگدر اصور البوتلودر لهالمقالة، ضفت code يخزن الframebuffer بملف وخليت مكان الخزن hardcoded لFAT32 partition بالهارد. ردت اخلي اسم الملف ينأخذ من الarguments، بس المشكلة انه الarguments دتجي char والFileName لازم يكون CHAR16 (لأن UCS-2). ملگيت function تحول بينهن، فمجرد اخذت اول حرف من الargument.

بالنسبة لوجود EFI_FILE_MODE_READ فهذا مجبور احطه:

ص515 من الUEFI 2.9 Specification

جربته بQEMU واشتغل، رحت للابتوب وشغلته بس بقه عاصي وكتب ملف فارغ. شكيت انه ملازم اقره من الframebuffer مباشرة فرحت اگلب بالspec ولگيت function اسمها Blt، هذا جزء من وصفها:

ص492

فعدلت الcode حتى يستخدم Blt ويه الBltVideoToBltBuffer operation، جربته وحصلت على ملف. جربت احوله لpng باستخدام ffmpeg (عرفنا الpixel format من الshell فوگ):

ffmpeg -f rawvideo -pixel_format bgr0 -s 800x600 -i x x.png

بس طلعلي هالخطأ:

شصار هنا؟ احنا حجم الملف اللي دنخزنه حسبناه باستخدام المعادلة الفوگ، المشكلة انه اخذنا الأبعاد من الGOP instance الاولى التعبانة اللي عبالها الresolution هي 1024x768، فصار حجم الملف الناتج 3145728 بايت. احنا گلنا لffmpeg انه الresolution هي 800x600، فعباله انه الحجم هو 1920000 بايت. فالتهم اول 1920000 بايت بس بقه 1225728 بايت وراها، شيسوي؟ گال هاي اكيد جزء من frame ثاني، راد يلتهمها عالواهس بس شاف انه 1225728 اقل من حجم الframe المتوقع (1920000) فگام يعيط. مو مشكلة، حنگله نريد frame واحد:

ffmpeg -f rawvideo -pixel_format bgr0 -s 800x600 -i x -vframes 1 x.png

فتحت الصورة وطلعت سودة، فتحت الملف اللي اخذناه من اللابتوب وطلع كله صفارة. شكلها الGOP instance الاولى حتى الBlt ما تدعمه عدل، فعدلت الcode حتى يستعمل الثانية بس للscreenshot و(أكيد مننسى FreePool ;)) وصارت عندي صور تمام.

حتى اطگطگ سكرينات للUEFI shell، مجرد اخذت الcode مال الscreenshot function وحطيته بالبداية وحطيت return (تعاجزت اسوي ملف من الصفر :p)، اسم الoutput file بقيته ثابت لأن تعاجزت اخذه من الarguments.