пʼятниця, 3 червня 2016 р.

Завантажувальні сервіси - Аґенти, споживання протоколів на гендлі

Такі функції як OpenProtocol(), CloseProtocol(), OpenProtocolInformation() були додані в UEFI пізніше, і були призвані розширити можливості фірмварі і прибрати небезпеки, які таїла старіша модель, в якій була лише функція HandleProtocol(), а деінстлаяціні й реінсталяційні сервіси могли нареінсталювати все дуже необачливо - без огляду на клієнтів, які покладались на ті протоколи, ясно, шо реінсталяція чи деінсталяція протоколу на гендлі, який використовується ще кимсь - це пагана ідея. Ну, от вони, ці сервіси й додали, мабуть. Більше безпеки. Але разом з тим, додали також мороки для втілення всього цього. Не в останню чергу через не найкраще подання в специфікації приправлене кумедними обдруківками, шо вилилось в доволі непростий шлях для розуміння шо ж мається на увазі.
Балакаючи про мій шлях в цьому, спочатку варто згадати, шо була додана одна зміна в структуру елемента БД, а саме перший елемент з списку агентів тепер сидить в самій структурі протокола, решта все як було - нефрагментована таблиця агентів. Це було зроблено через сементику того прив'язування агентів. Агенти "відкривають" протоколи на гендлах з певними атрибутами. Власне ось вони всі сім на поки:
  • BY_DRIVER
  • BY_DRIVER_EXCLUSIVE
  • EXCLUSIVE
  • BY_CHILD_CONTROLLER
  • GET_PROTOCOL
  • TEST_PROTOCOL
  • BY_HANDLE_PROTOCOL

Специфікація визначає, шо в гендла на якомусь конкретному протоколі завсіди може бути лише один агент з одним з трьох атрибутів - BY_DRIVER, BY_DRIVER_EXCLUSIVE, EXCLUSIVE.
Може й не бути жодного з цих трьох, як я розумію, але якшо є з тих трьох, то тільки якийсь один з них. Перший - це коли драйвер споживає протокол, другий це коли драйвер споживає з виключним правом, третій це коли з виключним правом споживає, якийсь застосунок (не драйвер). З назв ясно, шо "виключне" споживання має привілей, а саме - якшо вже є споживач з атрибутом BY_DRIVER, виключний споживач може його витурити. При чому зробити це детерміністично, а саме функція OpenProtocol() викличе в цьому випадку DisconnectController() для драйвери, який треба від'єднати, а та викличе Stop() сервіс в DRIVER_BINDING_PROTOCOL'і який має бути інстальований на гендлі від'єднуваного драйвера. От так робити правильно.
Не можу похвалитися, шо я вже чітко бачу всю цю модель, але от в цьому аспекті нащастя видно один момент - ці три типи споживання стоять окремо від решти, і наче як просяться на особливу роль і в організації підлеглих даних. Не просто так, а для оптимізації звичайно. Дійсно, а шо, якшо для цієї трійці, ми виділимо першу комірку в таблиці, і там тільки такі атрибути лежатимуть? А потім виникає продовження цієї ідеї - покласти цю комірку в саму структуру протоколу - це підвищує локальність - майже напевно саме ці агенти суть "найголовніші" і саме з ними буде найбільше волокіти - отже мати їх уже в протоколі, робить пошук їх константним замість лінійного. Отже зміна - голова таблиці агентів тепер всунута в структуру протокола. Для пришвидчення - це відображення на структурі даних особливої ролі таких агентів.
Варто також додати, шо чим більше роздумів і вияснення іде шодо агентів, тим більше вимальовується картина, в якій стійко звучить питання - їх взагалі всі оті атрибути будуть використовувати? Мається на увазі не взагалі використовувати, а так, шо дійсно треба тримати інформацію за всіх агентів, які відкрили протокол для тесту наприклад, або з іншим атрибутом. Адже навіть специфікація каже, шо такі агенти навіть не мають викликати CloseProtocol() до пари. Виникає питання - нашо тоді БД триматиме цю зайву інформацію? Шоб собі додати мороки? І того, думається, частина БД шо стосується агентів, може зазнати певних змін. В бік редукції - нам може бути не треба аж так багато записувати як думалося з першого прочитання.
Розгляньмо поштучно решту атрибутів.
TEST_PROTOCOL
Це для тесту чи підтримує цей гендл цей протокол. Чи він присутній. Клієнт не має ані викликати CloseProtocol(), ані постачати змінну куди буде вернутий вказівник на структуру інтерфейсу.
Питання: нашо тримати інформацію в БД, шо якийсь клієнт так відкривав протокол? Шоб зробити "безпечну" деінсталяцію/реінсталяцію протокола? Але жодних дій оповіщення такого клієнта не передбачено. Йому типу як всеодно. Єдина причина тримати інформацію за таке відкривання - це того, шо функція OpenProtocolInformation(), має вернути масив усіх клієнтів споживачів. Ага. Але це причина заради причини. Знову, отримавши такий масив, нікому - ані фірмварі, ані тому, хто викликав цю функцію, ані самому клієнтові який відкрив протокол для тесту, не буде від цього ніякої користи. Нема наприклад вимоги, оповіщувати якось клієнта, шо от протокол який ти тестував уже скоро буде невалідний. Висновок який напрошується - `нічого тут тримати в БД.
Специфікація так вельми довільно описуючи як цей "безпечний" варіант робитиме, в функції UninstallProtocolInterface(), каже, шо от від'єднавши (безпечно) драйвери на цьому протоколі, можна тоді "від'єднувати" тестувальниікв і решту. Але шо це значить для самих тестувальників. Звичайно, для БД формально, ми змінюємо - ми самі викликаємо CloseProtocol() для тестувальників, викреслюємо їх з БД, але шо це дасть? Кому це треба? Хто цю роботу оцінить, для кого це матиме якісь важливі наслідки? Ні для кого.
GET_PROTOCOL
Усе сказане в попередньому абзаці стосується так само і цієї опції. Різниця в тому, шо семантика тут уже не тестувальна зовсім, а куди серйозніше - специфікація каже, шо це для драйверів, шоб отримати доступ до протоколу. І тут же попереджає бути пильним, бо при цьому методі "драйвер не буде оповіщений про деінсталяцію або реінсталяцію протокольного інтерфейсу на гендлі". Просто фантастика! Нате ось вам інструмент для обвалювання системи, користуйтесь і не кажіть шо ми вас не попереджали... Але облишивши критику, можна сказати, імплементатор має миритися з вимогами стандарту. Тобто таки треба втілювати оце ось засовування пальця в розетку. Але знову - підтримка інформацї про таких агентів не має ніякого сенсу крім видати її незрозуміло для чого на спеціальному запиті. Ніякої функціональности поза цим немає.
[Додано пізніше] Звичайно, тут я трохи погарячкував, цей метод таки потрібен. Це не означає. шо він стає безпечним, або шо шось міняється в плані підтримки інформації за цей тип під'єднання в БД, не міняє, - просто цей метод виявляється потрібним. Коли я писав ConnectContoller(), стало ясно, шо GET_PROTOCOL потрібен -  нам потрібен саме вказівник на інтерфейс протоколу, але навряд нам потрібно відкривати його BY_DRIVER - це зробить пізніше Supported() або Start() самого протоколу. Намагаючись під'єнати драйвер до контроллера, ми перебираємо купу гендлів драйверів потенційних кандидатів. На рівні ConnectController(), ми маємо саме мати доступ до Supported() і Start(), цього протоколу. Як? Правильно, треба відкрити протокол. Не те шо б це було очевидно, але здається відкривати його BY_DRIVER було б тут паганою ідеєю. От ми й використовуємо той "ненадійний" GET_PROTOCOL. Мабуть я правильно відслідкував логіку введення його в специфікацію. Так же само, мабуть існує купа подібних ситуацій для драйверів і зстосунків, де їм треба інтерфейс протоколу, але їм не треба або вони не можуть відкривати його BY_DRIVER. Було б тільки краще, якби вони чіткіше роз'яснили це в описі цього атрибуту. Чого він ненадійний? Бо якшо хтось деінсталює/реінсталює цей протокол, клієнт, який відкрив так протокол, нічого не знатиме за це, і намагатиметься використовувати його й далі, а там уже все не так - халепа. Якшо ж ти відкриваєш BY_DRIVER, тебе принаймні буде оповіщено шо ти маєш від'єднатися. Або взагалі "конкуренти" отримають ACCESS_DENIED. Але це володіння протоколом не підходить для ConnectController(), яка насправді тільки намагається вияснити хто ж має під'єднуватися. Якшо вона відкриє BY_DRIVER, а далі передасть до Supported(), та можливо, не змігши відкрити цей протокол верне неуспіх. Все дуже складно, і пишучи це я навіть не розумію, чи я правильно розумію. Ясно одне - специфікація дуже плутана і складна в цьому місці. Хай там як, поки моя імплементація OpenProtocol() отримавши GET_PROTOCOL і впевнившись, шо цей протокол є присутній на гендлі, радісно вертає вказівник на його інтерфейс, нічого не змінюючи в БД. Стосовно ж небезпечности використання ConnectController()'ом вказівника отриманого через GET_PROTOCOL, думається, тут не має бути проблеми - СОП працюють на рівні TPL_NOTIFY, блокування заборонене. Якшо ж Supported() викличе UninstalProtocolInterface() або родичів, ну, це буде як молотком по компутеру - не встоє. Вона не має цього робити. Проти всіх можливих помилок нічого не зробиш. Звичайно, можна навіть проти цього протистояти - відкривати після Supported() і перед Start() OpenProtocol() на цьому інтерфейсі, але це було б уже геть тупо. Зрештою, семантично неправильно: ланцюжок успіху: OpenProtocol->Supported->Start мається на увазі на одному й тому самому протокольному зразку. З тим самим вказівником на інтерфейс. А не переставленим. Ми вважаємо, шо той, хто використовує GET_PROTOCOL, знає, шо вказівником на інтерфейс, який з цим атрибутом відкривається, можна користуватися "недовго". Бо теоретично, він може зіпсуватися.
BY_HANDLE_PROTOCOL
Ще один їжачок в тумані. Єдине, шо тут ясно - це зворотня сумісність. Тут таки вимагають я так зрозумів викликати CloseProtocol(), отже зроблений механізм споживачеві самому визначати точку інвалідизації інформації яку він споживає - він отримує інтерфейс, використовує його, сміливо, а потім, коли воно йому не треба - повідомляє про це систему, і вже не використовуватиме цей ресурс. Круто.
[Додано пізніше] Ні, тут я зрозумів неправильно. Ніхто нічого не вимагає - просто не може. Як? Це для сумісности з HandlePrototcol(), вона ні про яке CloseProtocol() не знає, просто вони забули згадати, шо цей варіант теж не вимагає викликати CloseProtocol(). Але головне, - взагалі, якшо просто деінде викликати CloseProtocol(), то ніякої безпеки не вийде - цю функцію має викликати саме клієнт який відкрив той протокол. Тільки тоді це має сенс. Якшо наприклад я, драйверописець, напишу драйвер, який намагатиметься відкрити протокол через BY_DRIVER, а в разі ACCESS_DENIED, викликатиму CloseProtocol() на агенті, який уже відкрив цей протокол і мені "заважає", (а це не секретна інформація, мій драйвер може викликати OpenProtocolInformation()), то уся система безпеки піде коту під хвіст - той агент не матиме й гадки, шо його від'єднали. Тільки якшо він сам викликає CloseProtocol(), це має сенс. CloseProtocol() просто "закриває" шо просиш, це "прибиральна", не "перевіряльна" функція, там нема ACCESS_DENIED. Знову - груба сила, шо його робити.
Далі за BY_HANDLE_PROTOCOL. З т.з. системи у цього варіанту можлива лише напередвизначений профіль самого агента - в якості агента виступає гендл самої фірмварі, а в якості гендла контроллера виступає NULL. Отже, на виході ми маємо тут фіксований атрибут, фіксовану пару агент/контроллер. Тільки змінна OpenCount тут може мати яксь варіації. Про цю змінну взагалі важко шо небудь сказати - шо вона має значити в контексті? Для чого вона? Шо ми знову бачимо - максимум, шо треба підтримувати в базі це Opencount для цього варіанту. З тим сенсом, шо цей лічильник даватиме нам можливість визначати чи є ще споживачі такого кшталту. Але от тут проблема виникає. Якшо у випадку за драйверами, тобто першою категорією, ми робимо реальне від'єднання з викликом DisconnectController() і дальшим ланцюжком, і клієнт справді змінює свій стан внаслідок цього від'єднання, в оповіщений щодо змін на протоколі, то тут, клієнт сам просто формально викликає CloseProtocol(), засвідчуючи свою дальшу нецікавість в протоколі. Якшо ми замість нього викликаємо цю функцію, як описано в UninstallProtooclInterface(), це ніяк не скаже клієнту про зміни, ніяк. Виходить, як і у випадку з GET_PROTOCOL - це небезпечна опція яка не має механізму себе убезпечити. Єдине її виправдання - це зворотня сумісність. Використовувати її в новому коді не треба взагалі. Користь від змінної OpenCount - суто статистична, ми просто знатимемо скільки таких клієнтів. І все. "Взула і забула". Того, подумавши, я взагалі нічого не додаю в БД з цим атрибутом. Знову - просто вертаю вказівник на інтерфейс якшо протокол інстальований на гендлі.
BY_CHILD_CONTROLLER
Єдина надія виправдати мання масиву агентів - це ця остання опція, яка очікується юзабельною, і дуже важливою для моделі драйверів UEFI. Я мало ще усвідомив за неї. Єдине це скоріше за все шинний драйвер, такий собі бонза в системі, відкриватиме так протокол для своїх "дітей". Отже це має бути треба. Має бути важливо. А не такий пустий формалізм і відбувайлівка як у вапидку з попередніми трьома опціями.
Отже, якшо я все правильно зрозумів, то наш масив протоколів трохи змінюється семантично.
Перше - в ньому виділяється "особливе місце" для відкривання драйвером (виключно теж) або застосунком виключно. Це перша категорія. Ми переносимо це в саму структуру протоколу. Тут ясно.
Друге - ніякої інформації про тестувальні відкривання а також відкривання через GET_PROTOCOL, BY_HANDLE_PROTOCOL, ми не тримаємо. Інформація в БД за це, нічого не робить нікому. Лише валятиметься в базі без діла і обслуговувальні функції, лише матимуть мороку возитися з цим.
Знову - ніякого убезпечення деінсталяції або реінсталяції для таких агентів ми зробити не зможемо - сама природа цього типу (BY_HANDLE_PROTOCOL), не дає це зробити - це зворотна сумісність - старий код просто викликає HandleProtocol() нічого не знаючи за оці всі агентські справи. І так усе тут і лишається з безпечністю як і було в часи коли той код був молодий. Але ми не маємо старого коду. Юху! Писатимемо наші драйвери по-новому, по-модньому. Ну і як сказав вище, OpenCount теж редуковано.
Четверте. Весь масив зводиться до елементів відкритих як BY_CHILD_CONTROLLER. Це полегшує. Але робить поле атрибуту в структурі баластом. Ну'.
Шодо виклику OpenProtocolInformation(), вона власне нічого не казатиме за BY_HANDLE_PROTOCOL, TEST_PROTOCOL і за GET_PROTOCOL, вона скаже шо нічого такого не знає і не бачила і не чула. А чого, я думаю я трохи пояснив угорі. Може це завалить тест відповідности (але ж до цього як до Києва рачки я вас балагаю), але зробить структуру і двигун БД вільним від штучок ради штучок, зробить простішим яснішим і таким, шо по ділу шось робить.

Немає коментарів:

Дописати коментар