Гипервизор сказал «нет»

Опубликовано 2026-09-19
Гипервизор сказал «нет»
Серия: Чей это компьютер?Часть 6 из 8

Заметки с bring‑up‑а собственной ОС на Samsung Galaxy Tab S6 (SM‑T865, Snapdragon 855 / SM8150). Продолжение статьи о «паразите» (карта всех жильцов устройства — во вводной серии): там мы выяснили, что Wi‑Fi‑прошивка на этом SoC DMA‑ит в нашу память по физическим адресам. Здесь — почему так вышло на уровне регистров, кто нам это разрешил, кто запретил сделать иначе, и как выглядит путь наружу.

01

Проблема

Записали в регистр SMMU тип bypass для DMA-потока Wi-Fi — прочитали обратно fault. Кто-то между нашей инструкцией и регистром посмотрел на значение и заменил его на противоположное. Это гипервизор Samsung на EL2.

02

Что сделали

Разобрали три уровня владения SMMU: stage 2 и глобальные регистры — гипервизора, содержимое банка stage 1 — наше. Повторили обход стокового Android: translate-банк с выключенной трансляцией. Тем же путём подняли USB, UFS и дисплей.

03

Что получилось

«Bypass» Android — это bypass, оформленный как трансляция: поток Wi-Fi-прошивки сегодня адресует физическую память напрямую. Из mainline известно, что stage 1 для этого чипа возможен — план записан, честно помечен как «не сделано».


0. Одна запись, которая не легла

История короткая. Прошивка WLAN живёт на Hexagon модема, её двенадцать copy engines ходят в RAM через apps_smmu (0x1500_0000) потоком 0x640 (в дереве устройств — iommus = <&apps_smmu 0x640 0x1>: два id, 0x640 и 0x641). Чтобы кольца DMA заработали, потоку нужно разрешение пройти. Самый простой способ на SMMUv2 — записать в регистр S2CR этого потока тип bypass: «не транслировать, пропускать как есть». Так мы и сделали.

Запись не легла. Регистр читался обратно с типом fault. Не «остался прежним» — переписан в fault. Кто‑то между нашей инструкцией и регистром SMMU посмотрел на значение и заменил его на противоположное.

Этот «кто‑то» — гипервизор на EL2. На Snapdragon он есть в каждом телефоне, и в стоковом дереве устройств у apps_smmu стоит свойство qcom,skip-init: «не инициализируй глобальную конфигурацию, она не твоя».

1. SMMUv2 за пять минут

ARM SMMUv2 (MMU‑500, как его ставит Qualcomm) — это MMU перед DMA‑мастерами. Транзакция приходит с stream id — номером, который NoC приписывает источнику. Дальше по порядку:

Путь одной транзакции и кто что в нём пишет:

 copy engine          +----------------- apps_smmu (0x1500_0000) -----------------+
 (Hexagon, WLAN)      |                                                          |
 ---- sid 0x640 ----> |  SMR[i]  ---->  S2CR[i]  ---->  CB n: CBAR CBA2R SCTLR   | ----> stage 2 ----> DRAM
                      |  id/mask        type,cb        TTBR0 TCR MAIR  (stage 1) |       (EL2)         PA
                      +------|--------------|-------------------|----------------+         |
                             |              |                   |                          |
 кто пишет:             мы (EL1)       мы (EL1),          мы (EL1):                  гипервизор
                        запись         но под вето EL2:   SCTLR.M = 0 сегодня,       (Samsung, EL2):
                        остаётся       BYPASS -> FAULT    M = 1 -- план              таблиц не видим

 поток без SMR -----> sCR0.USFCFG (глобальный регистр: гипервизора) -----> bypass | fault

Нам принадлежит середина — SMR, S2CR (с оговоркой) и содержимое банка. Глобальные регистры и stage 2 — нет.

2. Что значит «гипервизор владеет глобальной конфигурацией»

qcom,skip-init в дереве — инструкция драйверу: не сбрасывать SMMU, не трогать sCR0, не считать себя первым. Кто‑то настроил устройство до EL1 и продолжает за ним следить. На планшете это гипервизор Samsung в hyp_mem (резерв no-map с 0x8570_0000).

Три уровня на практике:

Стоковый Android живёт в тех же условиях — с той же строкой в дереве и тем же гипервизором.

Та же картина на уровень выше — не регистры, а жильцы: кто какой слой apps_smmu держит и куда через него доходит поток 0x640. Красное — чужое, зелёное — наше; сплошная стрелка — механизм, наблюдавшийся живьём, пунктир — «может, если захочет» и то, что помечено как план. Это фрагмент общей карты устройства из вводной статьи (§2a), теми же именами узлов.

Схема: Гипервизор сказал «нет»

3. Вето

Наблюдаемый факт: запись S2CR.TYPE = BYPASS возвращается как FAULT. Гипервизор не разрешает EL1 объявить поток «без трансляции» напрямую. Почему именно — политика «каждый поток через банк» или защита от гостя, который случайно откроет мастеру всё, — мы не знаем. Знаем, что на этом SoC так не пишется.

Драйвер различает две неудачи. Если банк‑кандидат был, а проверка после записи показывает не то, что писали, — SmmuError::Vetoed: записи не легли. Если свободных банков не нашлось вовсе — NoRoom.

4. Обход: pass‑through в маске трансляции

Что делает стоковый Android для мастера без stage‑1 таблиц (icnss со свойством qcom,smmu-s1-bypass) и что повторяет наш map_passthrough_group:

  1. Найти SMR, уже называющий sid, иначе свободный; записать 0x640 с маской 0x1.
  2. Найти контекст‑банк, на который не указывает ни один валидный S2CR типа translate — не занятый ни гипервизором, ни другим мастером. Кандидатов перебирается не больше восьми.
  3. В CBAR — «stage 1 с обходом stage 2» (гипервизор свой stage 2 подставит сам), атрибуты non‑shareable, write‑back; в CBA2R — 64‑битный формат.
  4. В SCTLR — ноль. M = 0: трансляция выключена, атрибуты входящие. Банк есть, таблиц нет, адрес проходит как пришёл.
  5. В S2CR — тип translate с номером этого банка.
  6. Прочитать disposition(sid) и убедиться, что поток теперь Translate { cb: наш, sctlr.M = 0 }. Нет — следующий кандидат; кончились — Vetoed.

Гипервизор это пропускает: с его точки зрения поток идёт через банк, по правилам. С точки зрения мастера адрес после stage 1 не изменился. Это bypass, оформленный как трансляция.

Тем же путём у нас идут все DMA‑мастера: USB (0x140), UFS (0x300), QUP/GPI на шинах I²C. Часть банков достаётся от загрузчика: UFS живьём читался как Translate { smr 2, cb 1, sctlr 0xe0, fsr 0x400 } — банк с M = 0, уже pass‑through; 0x400 — бит 10 FORMAT, признак формата, а не fault: банк ничего не записал. Такой банк map_passthrough_group оставляет и лишь чистит FSR. Дисплей (0x800) на этой загрузке — тоже pass‑through с SCTLR.M = 0.

В таблице ресурсов платформы это одна строка — smmu:wlan, «wlan firmware dma: apps smmu stream 0x640/1», переключатель Switch::Stream. «Включено» для неё — Bypass или Translate с M = 0; «выключить» — пустая операция, включённое остаётся включённым. Голосует wifi.power до старта модема: первое обращение прошивки в память через несконфигурированный поток под гипервизором — не ошибка, а тихий сброс всего SoC (на CAL_REPORT).

5. Что защищает stage 2 гипервизора — и чего не защищает

От чего защищает. От гостя и его мастеров — память гипервизора, secure world, регионы других процессоров, всё, чего в его таблицах для нас нет. Что именно там есть, мы не знаем и проверить не можем.

От чего не защищает по построению. Stage 2 отображает память гостя. Наше ядро, куча, стор с паролями сетей и токенами, кольца DMA в LENT_WLAN — для гипервизора это одна сущность, «RAM гостя»; он не отличает наш код от нашего DMA‑буфера. Если бы отличал — наш собственный pass‑through не работал бы. Значит, всё, куда может писать наш процессор, может писать и мастер за банком с M = 0.

Отсюда формулировка модели угроз: по нашей части системы поток 0x640 сегодня адресует физическую память. Адрес в SR_BASE_LO/HI и DR_BASE_LO/HI колец — физический адрес LENT_WLAN (0x84e0_0000, 4 МиБ некэшируемой RAM ниже hyp_mem). Честная прошивка пишет туда. Скомпрометированная положит в дескриптор любой другой адрес — и на уровне apps_smmu её ничто не остановит. Оставшиеся заборы — stage 2 Samsung и XPU Qualcomm — не наши, мы не видим их содержимого и не можем считать их своей границей.

6. Линза, а не драйвер

Поскольку SMMU не наш, Smmu намеренно почти ничего не делает. Это инструмент наблюдения плюс одна операция:

Никаких «сбросить SMMU», «включить порт», «выставить USFCFG». Глобальное состояние (sCR0, IDR0/1, sGFSR) читается, не пишется. Каждый мастер при подъёме сообщает свою disposition до и после — в логе bring‑up‑а, в фактах hw.ufs и hw/panel. Так «кто владеет чем» не теряется между запусками.

Про USFCFG одно замечание: поток без SMR при взведённом бите роняется, и на этом SoC такой fault для процессора — не событие, а зависание. Поэтому «просто не трогать SMMU» для нового мастера — не вариант; каждому нужен явный SMR и банк.

Отступление: питание единиц трансляции

SMMU — не один блок. Единицы трансляции (TBU) сидят на NoC рядом с мастерами, и у них свои домены питания в GCC: tbu1_gdsc и tbu2_gdsc (hlos1_vote_aggre_noc_mmu_tbu*_gdsc; TBU2 — потоки 0x400..0x800 на aggregate NoC, TBU1 — ниже). Загрузчик оставляет их выключенными. Симптом — не fault: первое обращение мастера в память ждёт вечно, и процессор, написавший ему doorbell, ждёт вместе с ним. В таблице ресурсов оба домена vital — на них держится консоль по USB, и выключить их сверху HAL отказывается. «Работает ли SMMU» на этом чипе — вопрос не только конфигурации, но и питания.

7. Дальше: настоящий stage 1 для 0x640

Это план. Живьём не сделано.

Идея прямая: тот же банк, тот же S2CR типа translate — но SCTLR.M = 1 и заполненные TTBR0/TCR/MAIR. Таблицы отображают только окно LENT_WLAN, identity внутри (IOVA = PA) и ничего снаружи. Кольца программируются теми же адресами — для честной прошивки ничего не меняется. DMA за пределы окна — fault банка (CFCFG: terminate, не stall), а не запись в память. disposition(0x640) должен показать Translate { sctlr.M = 1 }, clear_faults в честной работе — ноль.

В коде: Smmu::map_translate(sid, mask, ttbr0, tcr, mair) рядом с map_passthrough_group; таблицы stage 1 на окно; Switch::TranslatedStream вместо Switch::Stream для строки smmu:wlan. Затем prove‑run: wifi.power on до ready, скан, вход в сеть.

Риски названы честно. Гипервизор может ветировать банк с M = 1 так же, как ветирует bypass. Адреса, которые прошивка вычисляет сама (память по SERVICE_READY), могут оказаться вне отображённого — тогда либо другой IOVA‑layout, либо честная запись, что путь на этой плате закрыт. Любое отклонение здесь, как и весь bring‑up, — тихий сброс.

Почему мы считаем, что это должно работать — вывод из mainline, не наше наблюдение. В mainline Linux sm8150.dtsi даёт wifi@18800000 iommus = <&apps_smmu 0x0640 0x1>. Драйвер arm-smmu-qcom.c держит список клиентов, которым выдаётся identity‑домен — mdss, adreno, *-mss-pil; qcom,wcn3990-wifi в нём нет. Значит, DMA API для этого устройства идёт через dma-iommu, и copy engines адресуют IOVA. HDK SM8150 с той же прошивкой WLAN.HL.3.2.0 поднимает Wi‑Fi в mainline. Это говорит, что stage 1 для 0x640 на этом SoC с этой прошивкой возможен и гипервизор референсной платы его не запрещает. Наш планшет — Samsung со своим гипервизором; совпадает ли политика, покажет только прогон.

И чтобы не переоценить результат: stage 1 запирает copy engines — путь через apps_smmu. Регионы, отданные модему через memshare (MSA, буфер rmtfs), он пишет своим путём — Q6 через собственную MMU и XPU, минуя apps_smmu. Там лечение другое: минимальные гранты, а забор — XPU от TrustZone.

8. Факт, план, вывод

SMMU в вашем телефоне — не ваш. Вам дают один слой из трёх и следят, чтобы вы не выключили его совсем. Пока этот слой у нас пустой, единственный забор между Wi‑Fi‑прошивкой и памятью ядра — чужой. Заполнить его — один банк, одна таблица страниц и один прогон, который либо подтвердит вывод из mainline, либо добавит строку в список того, что этот гипервизор не разрешает.


Источники. Драйвер‑линза: crates/qcom/src/smmu.rsDisposition, map_passthrough_group, SmmuError::{Vetoed, NoRoom}, clear_faults, FSR_FAULTS, смещения регистров. Платформа: platforms/sdm855/src/soc.rsAPPS_SMMU, WLAN_SID, MDSS_SID, LENT_WLAN, TBU1_GDSC/TBU2_GDSC; platforms/sdm855/src/resources.rs — строка smmu:wlan; crates/qcom/src/resource.rs — семантика Switch::Stream; platforms/sdm855/src/storage.rs — живое чтение банка UFS; crates/qcom/src/i2c.rsUSFCFG как зависание. Документы: docs/wifi-sm8150.md (таблица фактов, §8 «чего с AP не видно»), docs/wifi-threat-model.md §4, §5 (уровень 2), §6, §7, docs/wifi-parasite-article.md §2.2, §8.

Что это значит для вас
SMMU в вашем телефоне — не ваш: вам дают один слой из трёх и следят, чтобы вы не выключили его совсем. Пока этот слой пуст, единственный забор между Wi-Fi-прошивкой и памятью ядра — чужой. Мы умеем читать эти заборы на любом устройстве, а в своих ставим собственный.

Продукты из статьи

Устройство, где хозяин — вы

Мы строим умные дома на 0xd-os — и проводим аудит чужих устройств и прошивок с письменным scope. Напишите, что у вас.

Часть 6 из 8
  1. 1Чей это компьютер? Ваш, или…
  2. 2Сброс без свидетелей
  3. 3Мегабайт, который убил модем
  4. 4Паразит в планшете
  5. 523 миллисекунды, чтобы солгать
  6. 6Гипервизор сказал «нет»
  7. 7Пароль у нас, ключ у модема
  8. 8Доверяй проводу?
0xd-ossecurityembeddedsmmu