Заметки с bring‑up‑а собственной ОС на Samsung Galaxy Tab S6 (SM‑T865, Snapdragon 855 / SM8150). Продолжение статьи о «паразите» — Wi‑Fi‑прошивке, которая живёт на модеме и предъявляет хосту требования. Здесь — о том, что происходит, когда требование не выполнено, и как это вообще можно увидеть.
0. Затравка: консоль замолчала
Рукопожатие с WLAN‑прошивкой идёт по плану. Через QMI ушёл MSA_INFO, secure world перевёл регион памяти в собственность модема, CAP вернул версию прошивки, BDF_DOWNLOAD отдал пять сегментов board data, CAL_REPORT принят со статусом SUCCESS. Осталось дождаться FW_READY_IND.
Вместо неё консоль по USB замолкает. Через секунду планшет показывает логотип загрузчика и стартует заново. В логе — ничего: последняя строка — про CAL_REPORT. Обработчик паники не вызывался. Вектор исключений не срабатывал. Ни одно прерывание не пришло. Watchdog‑ядро, которое должно описать зависание, не оставило вердикта — оно умерло вместе со всем чипом. PMIC говорит ровно то, что говорит после любой штатной перезагрузки.
Таких эпизодов за bring‑up Wi‑Fi было около десяти. Причины оказались разными — обслуживание кэша, куча, рельса питания, невключённый такт, чтение на четыре байта дальше положенного. Общим было одно: SoC отвечает на ошибку хоста не ошибкой, а перезагрузкой, и не оставляет улик. Эта статья — о том, как в таких условиях всё‑таки доказывать причину.
1. Почему свидетелей нет
Все привычные средства посмертной диагностики опираются на одно из двух: либо ядро продолжает исполняться хотя бы несколько инструкций после ошибки (паника, обработчик исключения, вердикт watchdog‑ядра), либо RAM переживает перезагрузку (кольцо лога, ramoops). Сброс из secure world не даёт ни того, ни другого.
Кто сбрасывает. Между нашим ядром (EL1) и памятью на этом SoC стоят два чужих слоя: гипервизор Samsung (EL2, stage‑2 трансляция) и TrustZone Qualcomm (EL3, XPU — аппаратные защитные блоки над регионами памяти). Нарушение, которое замечает любой из них — запись в страницу, которую TZ пометил своей, или спекулятивное чтение в carve‑out другого ядра, — обрабатывается не как исключение для нормального мира, а как решение о перезагрузке всего чипа. Нормальному миру ловушка не доставляется; по крайней мере, ни одного такого исключения мы не видели. Точный механизм внутри TZ нам недоступен, поэтому дальше «сброс из secure world» — это описание наблюдаемого поведения, а не утверждение о коде Qualcomm.
DRAM не переживает. Это было проверено, не предположено: собственная NOLOAD‑страница образа, последние 64 КиБ нашей RAM, свободная память на 2,5 ГиБ, даже samsung’овская область ramoops — каждая загрузка считала себя в каждом из этих мест, и каждая приходила чистой. Загрузчик S‑Boot возвращает DRAM пустой после любого warm reset. Полдня пустых вердиктов ушло на то, чтобы это выяснить.
PON‑регистры PMIC — не свидетель. Блок power‑on PM8150 честно записывает, почему чип включился и как в последний раз выключался: PON_REASON1, WARM_RESET_REASON1/2, POFF_REASON1/2, OFF_REASON, FAULT_REASON1/2, S3_RESET_REASON. Проблема в том, что сброс из secure world оставляет в них ту же картину, что и PSCI SYSTEM_RESET, который мы сами вызываем штатно: hard reset как причина включения, warm reset, упорядоченная последовательность выключения (POFF_SEQ). Биты FAULT_N в FAULT_REASON при этом присутствуют, но они липкие — держатся до следующего события своего рода — и относятся к более ранним эпизодам. То есть PON отличает нажатие кнопки от падения батареи и от того, что SoC сам дёрнул PS_HOLD, но не отличает «мы попросили перезагрузку» от «нас перезагрузили».
Лога TrustZone нет. У Qualcomm есть журнал TZ в IMEM, и узел tz-log есть в дереве устройств. На Samsung он зашифрован, и IMEM чистится при сбросе. Попытка прочитать его через отладочный инструмент дала одиннадцатый эпизод вместо улик — об этом в § 3.
Watchdog ловит зависания, не сбросы. На плате два watchdog’а: аппаратный APSS (qcom,wdt), заведённый на 10 с до чтения дерева устройств, и программное watchdog‑ядро — второй процессор, поднятый через PSCI CPU_ON, который четыре раза в секунду смотрит на датчик планировщика и через 8 с одного и того же poll’а пишет вердикт, рисует его на панели и через 30 с перезагружает машину. Это хорошо работает, когда основное ядро застряло на шине, которая не отвечает. Но сброс — не зависание: наблюдать нечего и некому.
Собранное вместе выглядит так: сверху — трое, кто может перезагрузить или обесточить чип, не отчитываясь; посередине — DRAM, которую любой из этих сбросов стирает; внизу — единственная микросхема, до которой сброс не дотягивается. Сплошные стрелки — механизм, пунктир — «может, если захочет»; жирная обводка — узлы, вокруг которых построена эта статья. Полная карта жильцов устройства, теми же именами узлов, — во вводной, § 2a.
2. Свидетель, которого построили
2.1 Где хранить
В момент сброса у нас нет ни строки лога, ни регистра, ни исключения; переживает событие только то, что записано до него в память, до которой сброс не дотягивается. PMIC работает от батареи и через сброс SoC проходит целиком. Что он держит:
| Место | Размер | Что там лежит у нас |
|---|---|---|
| Данные будильника RTC | 4 байта | крошка (Crumb): номер шага загрузки, стадия (mmu / dtb / bringup / heap / modules / run), метка сборки; либо Ready, Panic, Hang, Clean |
SDAM sdam@b100, +0x00…0x1f | 32 байта | ячейки вендора (причина рестарта на 0x08, слово на 0x14, которое прошивка переписывает каждую загрузку) — не трогаем |
SDAM +0x20 | 16 байт | испытания (Trials): какой шаг bring‑up сейчас под пробой; сборка, погибшая с взведённой пробой, дальше грузится безопасным путём (hw.trials) |
SDAM +0x30 | 8 байт | смещение настенного времени от секунд RTC — чтобы time.set пережил перезагрузку без сети |
SDAM +0x38 | 72 байта | последние слова (Words<64>): 8 байт заголовка (магия + длина) и до 64 байт текста |
SDAM (shared direct access memory) — периферийный блок PMIC, который не делает ничего, кроме хранения байтов; Android видит его как nvmem. Запись идёт по SPMI, по 8 байт за команду, через канал арбитра, которым владеет наш execution environment (если канал не наш, слово читается, но не пишется, и свидетеля у нас нет — это тоже проверяется при старте).
2.2 Три слоя слов
В 72 байтах «последних слов» живут записи двух видов, различаемые магией заголовка:
- Слова смерти (
PANI): текст паники — файл, строка, начало сообщения — или вердикт watchdog‑ядра (hang: \call:x` polled 8 s at pc 0x…`). Пишутся из обработчика паники или с watchdog‑ядра, без кучи (паника может быть аллокатора). Первые слова прогона остаются: вердикт watchdog’а через 8 с не перекрывает панику, из‑за которой ядро и остановилось. - Метка (
MARK): имя шага, который начался и ещё не закончился. Именно она — свидетель тихого сброса.
Метки бывают двух уровней, и оба живут в датчике планировщика (sched::gauge), а не в платформе:
gauge::marked("pas auth")— охранник для короткого шага, который может повесить ядро с замаскированными прерываниями: вызов в secure world поsmc, первое чтение SMEM, регистр блока, такт которого может быть выключен. Пока охранник жив, слово — его имя; при drop метка снимается. Так помеченыscm detect,scm pas,pas init,pas place,pas auth,pas shutdown,qmp open,qmp load_state,smem toc,smem header,smem items,smp2p edge,glink attach,glink poll,glink send.gauge::Phase::begin("wlfw ind_register")/phase.set("wlfw cap")— фаза длинной операции, которая тянется через многоawaitи опросов планировщика: рукопожатие WLFW, включение таргета. Её ставит и двигает сама операция, снимает drop. Метки вкладываются поверх фазы: пока охранник жив, слово — метка; когда он снят — снова фаза.
Обе — это указатель и длина на &'static str в атомиках плюс один слушатель (gauge::on_mark). Плата подписывает на него persist::mark: на постановку слова — SPMI‑запись заголовка и имени, на снятие — восемь нулевых байт. Одна‑ две записи по SPMI на шаг, +1,5 КиБ к образу микроконтроллерной платы, где Phase тоже собирается. Это вся цена.
2.3 Что читается при следующей загрузке
persist::open идёт первым делом после того, как поднят арбитр SPMI: читает PON, крошку, SDAM, тут же чистит запись слов (чтобы вердикт вышел один раз) и отдаёт всё crash::death. Тот складывает вердикт из трёх источников:
- есть слова смерти → причина
panicилиwatchdog, текст — как оставлен; - слов нет, крошка
Ready, PON не говорит ни о кнопках, ни оPS_HOLD→reset: «the last run was up when it ended, and left no words»; - и в любом случае, если стоит метка → к вердикту дописывается: «it was in
wlfw fw_ready wait— a step marked as one that can stall the core …, and the mark was never cleared».
Ответ виден в sys.boot (last_reset, previous {reason, died_at, step, words}) и в истории sys.crashes. Первая команда после каждой перезагрузки в этой работе — sys.boot.
2.4 Что фаза доказывает, а что нет
Доказывает одно: шаг с таким именем начался, и до сброса его конец не был записан. Разрешение — один шаг: если фаза стоит на wlfw fw_ready wait, это шаг после последнего запроса — значит, наши команды прошивке уже все ушли и были приняты, а сброс пришёл, пока мы ждали её ответа. Это сдвигает подозрение с «мы неверно послали» на «прошивка сделала что‑то по нашему запросу и наткнулась на препятствие». В случае № 8 из таблицы ниже ровно эта логика привела к тактам.
Не доказывает: кто сбросил и почему. Метка не различает «TZ перезагрузил на этом шаге» и «ядро повисло на этом шаге, а через 10 с укусил аппаратный watchdog» — здесь помогает PON (бит PS_HOLD в WARM_RESET_REASON1 — признак, что SoC сам дёрнул линию), но, как сказано выше, полагаться на него при сбросе из secure world мы не можем. Не говорит она и о задержке: сброс № 2 пришёл через 6–10 с после шага, который его вызвал, и никакой фазы в тот момент не стояло — только крошка Ready. Свидетель говорит, где мы были, а не что произошло.
3. Сбросы по одному
Таблица ниже — центр этой статьи. Оговорка: не все её строки — сбросы SoC. Пять из десяти (№ 1, 2, 3, 8, 9) — перезагрузки всего чипа без слов; № 4 — паника у нас плюс перезапуск модема; № 5, 6 — смерть модема (тоже без слов: до старта его сервиса ошибок причина не публикуется); № 7 — единственный случай, когда secure world отказал, а не сбросил; № 10 — риск, закрытый до того, как проявился. Они собраны вместе, потому что метод один.
| # | Симптом | Цепочка рассуждений | Чем доказано | Фикс |
|---|---|---|---|---|
| 1 | Сброс через ~10 с после первого remoteproc.list, даже в простое | сброс приходит от времени, не от действия → кто‑то трогает память после нас → это либо hyp, либо XPU → единственное разделяемое, что мы тронули, — SMEM, и мы обслуживали по нему кэш | две контрольные прошивки: без мьютекса и без звонка модему — умирает; без dc civac/dc cvac — живёт | SMEM — некэшируемое окно, Coherent, ни одной инструкции обслуживания кэша по разделяемому |
| 2 | Сброс через 6–10 с после старта модема, ядро в простое, меток нет | шаги PAS все прошли (метки сняты) → сброс не на вызове, а после → что TZ продолжает читать после auth_and_reset? метаданные образа → они лежали в Vec в куче → куча освободила и переиспользовала буфер → запись в страницу под XPU | референс: qcom_scm_pas_metadata_release вызывается после auth (с Linux 5.18); тайминг — сброс в простое через 6–10 с после старта, когда куча уже переиспользует память | LENT_PAS — статическое некэшируемое окно вне кучи |
| 3 | Сброс через несколько секунд после handover, PMIC «чист», укусил watchdog | «оба ядра молчат, без меток, без слов» — не XPU, а остановка всего интерконнекта → питание → на handover мы снимали proxy‑голос RPMh за cx.lvl до нуля → vdd_cx питает всю цифровую логику SoC → пока модем занят, агрегат держится на его голосе; модем в idle → CX проваливается | разбор rpmhpd референса; корреляция: модем «занят» — живёт, модем «в простое» — сброс | пол уровней FLOOR_LEVELS = [NOM, 0]: cx.lvl не ниже NOM, mss.lvl — 0 |
| 4 | panic: mutex locked pre-entrantly; модем читает пустые заголовки EFS, восстанавливается из «золотой» копии и падает на «Graceful Restart» ~20 с | выглядело как баг rmtfs → но тень на диске валидна → значит, модем читает не то, что мы пишем → буфер rmtfs частично кэшируем → окно LENT начиналось с 0x8530_0000, не на границе 2 МиБ: первый МиБ попал в кэшируемый блок | расчёт блоков identity‑map; наблюдённая последовательность чтений EFS модема | LENT с 0x8520_0000; const‑ассерты на выравнивание всех окон |
| 5 | Модем fatal ровно через 23 мс после OPEN /boot/modem_fsg | раздела fsg на планшете нет → мы отвечали отказом, как rmtfs в Linux → для этой прошивки модема отказ фатален | таймлайн запросов в ipc.hosts и лог состояний remoteproc: 23 мс, каждый раз | fsg открывается пустым (нули = стёртая золотая копия, EFS это состояние знает) |
| 6 | Сервис WLFW (0x45) не появляется; модем fatal на ~40 с без причины | сервисы 66/43 есть, 0x45 нет → WLAN‑PD не стартует → чего он ждёт? ответа на kernel/elf_loader от pd-mapper и записи server_check.txt через tqftp; отдельно — без rmtfs EFS sync валит модем на 40‑й секунде | ipc.services; на планшете один modemr.jsn (root PD), карты доменов для WLAN нет | rmtfs, pd-mapper с фактом hw/pd, tqftp со scratch‑директорией |
| 7 | mem.assign для MSA отвергнут secure world | адрес 0x8bc0_0000 взят из офлайн‑dtsi другого релиза → в живом дереве по этому адресу память гипервизора | sys.hw живого pil_wlan_fw_region = 0x8c20_0000 | Msa::resolve читает адрес только из живого дерева |
| 8 | Сброс после CAL_REPORT; фаза в SDAM — wlfw fw_ready wait | все наши запросы приняты → прошивка в ответ на CAL_REPORT что‑то трогает → сначала подозрение на MSA: адрес сверен живьём, размер 1,5 → 1 МиБ по qcom,wlan-msa-memory — сброс остался → значит, не память → что ещё включает референс перед этим шагом? ath10k_snoc_clk_enable: такты cxo_ref_clk_pin (rfclka2) и qdss через AOP | gauge::Phase → sys.boot; воспроизводимость с 1 и 1,5 МиБ, с верным адресом и с потоком SMMU; snoc.c + sm8150.dtsi | строки rfclka2 и aop:qdss в таблице ресурсов, голосуются wifi.power |
| 9 | Сброс при sys.hw {node: tz-log, peek} | узел tz-log заявляет 0x146bf720 + 0x3000, а родительский IMEM — 0x146bf000 + 0x1000 → окно узла выходит за конец IMEM → чтение за концом | сравнение reg узла с reg родителя | hw.peek не выходит за reg родителя; TZ‑лог у Samsung всё равно зашифрован |
| 10 | Не наблюдён; закрыт превентивно | Normal‑маппинг (даже некэшируемый) допускает спекулятивное чтение; Cortex‑A76 читает вперёд; над hyp_mem и carve‑out’ами ядер это увидит XPU | анализ блоков карты памяти; правило 4 из § 2 документа по Wi‑Fi | RESERVED = Device с 0x8560_0000 (на МиБ раньше hyp_mem, чтобы блок не остался Normal); carve‑out модема — Device после загрузки |
3.1 Кэш и MMU: № 1, 4, 10
Общий корень — identity‑map ядра делит гигабайт с RAM на блоки по 2 МиБ с разной кэшируемостью. Всё, что читает или пишет другой мастер — модем через DMA, secure world, гипервизор, — должно лежать в блоке Normal Non‑cacheable или Device, и весь блок целиком. Сброс № 1 научил не трогать кэш по разделяемому вовсе (Linux на этом месте делает no-map + ioremap_wc — то же самое другими словами). Сброс № 4 показал, что «окно некэшируемое» не значит ничего, пока окно не начинается на границе блока: половина буфера оказалась в соседнем кэшируемом блоке, модем прочитал из DRAM то, что мы ещё держали в кэше, — нули, — и повёл себя рационально: восстановил EFS из «золотой» копии. Симптом был на два слоя выше причины. № 10 — вывод из первых двух, сделанный до третьего сброса: no-map дерева переводится в Device, потому что предвыборка A76 в Normal‑область — это чтение, а XPU не спрашивает, было ли оно намеренным.
3.2 Владение памятью: № 2, 7, 9
Здесь противник — XPU. Правило одно: всё, что secure world читает или охраняет, — вне кучи и вне окон, которые мы переиспользуем. № 2 — самый трудный из десяти: сброс отделён от причины на 6–10 секунд и на несколько аллокаций, меток на нём нет, потому что все шаги PAS формально завершились. Помог референс: в Linux метаданные PAS освобождаются после auth_and_reset, и это не случайность — TZ читает их ещё секунды после возврата. № 7 — редкий случай честного отказа: неверный адрес MSA был памятью гипервизора, и assign_mem вернул ошибку вместо сброса. Из этого сделан вывод общего характера: адреса регионов берутся только из живого дерева устройств, потому что между релизами прошивки они плавают. № 9 — сброс, вызванный отладочным инструментом: tz-log заявляет 12 КиБ там, где у IMEM 4, и чтение за концом блока — тоже нарушение. Инструмент теперь проверяет границу родителя.
3.3 Питание и такты: № 3, 8
Эти два — о том, что прошивка модема и SoC держат ресурсы через RPMh и AOP по голосованию, и хост — один из голосующих. № 3: сняв свой голос за cx.lvl, мы затёрли голос, который XBL оставил от нашего имени; система жила ровно пока модем был занят и голосовал сам. Симптом здесь отличается: это не сброс от TZ, а остановка интерконнекта — оба ядра замолкают, аппаратный watchdog кусает. Но с точки зрения улик разницы нет: «без меток, без слов, PMIC чист». № 8 — тот случай, ради которого фаза в SDAM и была написана. Без неё сброс после CAL_REPORT выглядел бы как сброс «где‑то в рукопожатии»; слово wlfw fw_ready wait сказало, что все наши команды уже приняты. Инструменты бисекции для этого места: wifi.power {on, until: ind_register | cap | bdf} останавливает рукопожатие после названного шага (cap — без board data и CAL_REPORT); убрать elf_loader из фактов — WLAN‑PD не стартует, и если сброс исчезает, виновник в пути WLFW. Картина сложилась такая: после CAL_REPORT прошивка инициализирует радио и трогает блок, чей такт хост не включил; обращение к незатактированному блоку — bus error, и SoC уходит в сброс — воспроизводимо, с любым размером MSA и с верным адресом. Референс snoc.c называет два такта; после их включения FW_READY_IND пришла через ~8 с после старта модема.
3.4 Ответы модему: № 5, 6
Строго говоря, это не сбросы SoC — умирает модем. Но умирает так же молча: причина смерти публикуется в SMEM его сервисом ошибок, который на этих стадиях ещё не запущен, так что fatal без причины всегда значит «рано». Свидетель тут другой — таймлайн запросов модема к нашим серверам (ipc.hosts): что он спросил последним и через сколько миллисекунд умер. 23 мс между отказом на fsg и fatal — это не совпадение, это реакция.
4. Уроки
Каждое разделяемое окно — некэшируемое и на границе 2 МиБ. Три из десяти строк — об этом. Проверяется const‑ассертами при сборке и debug_assert в настройке MMU; ошибка выравнивания больше не может дойти до платы.
SoC наказывает отклонение сбросом, а не ошибкой. Из десяти случаев только один (№ 7) дал код ошибки. Отсюда следствие для метода: гипотезы проверяются не по сообщению, которого не будет, а по разности — две контрольные прошивки с одной убранной переменной (№ 1), корреляция с состоянием модема (№ 3), остановка цепочки после шага (№ 8). Одна прошивка — около двух минут; поэтому гипотезу стоит проверять самой дешёвой правкой, прежде чем строить целевую форму.
Референс — не документация, а протокол. В четырёх случаях (№ 2, 3, 6, 8) ответ был в исходниках Linux — не в комментариях, а в порядке действий: когда освобождаются метаданные, какие голоса снимаются на handover, какие такты включаются до CAL_REPORT. Порядок там оплачен теми же сбросами.
Слово о фазе дешевле отладчика. JTAG на потребительском планшете недоступен, TZ‑лог зашифрован, DRAM не переживает. За полторы тысячи байт кода и одну SPMI‑запись на шаг мы получили единственный источник, который отвечает на вопрос «где мы были». Это не заменяет рассуждений — но без него № 8 пришлось бы бисектировать вслепую по десятку шагов рукопожатия, по две минуты на прошивку.
Честный список того, чего не видно. LDO PMIC читаются только наблюдателем (каналы принадлежат AOP), TZ‑лог зашифрован, IMEM чистится при сбросе, регистры QDSP6SS под PAS — bus error при чтении, SMMU глобально принадлежит гипервизору. Диагностика проектируется от этого списка, а не от того, что хотелось бы иметь.
5. Сброс как способ проверить хозяина
В статье о паразите Wi‑Fi‑прошивка описана как жилец с правами арендодателя: она требует у хоста память, файлы, диск, IPC, семь рельс и два такта, и отказать ей нельзя. Сбросы из этой статьи — то, как выглядит проверка этих требований. Она не приходит в виде кода ошибки, который можно обработать. Хост либо выполнил условия — и тогда через восемь секунд приходит FW_READY_IND, — либо чип перезагружается, и хозяину предлагается самому догадаться, чем он не угодил.
Метка в PMIC — способ хотя бы запомнить, на каком вопросе нас перезагрузили. Сами ответы — карта памяти, серверы, рельсы — описаны в соседних статьях (Мегабайт, который убил модем, 23 миллисекунды, чтобы солгать, Гипервизор сказал «нет»); карта всех жильцов устройства — во вводной. Здесь важно другое: у современной SoC есть слой, который принимает решения о хосте и не отчитывается перед ним, и единственная стратегия, которая с этим работает, — писать слово перед каждым шагом, а не надеяться прочитать что‑то после.
Источники. Таблица сбросов, карта памяти и методика — docs/wifi-sm8150.md (§ 0, § 2, § 7, § 8); границы наблюдаемого — docs/wifi-threat-model.md § 8; метки и фазы — crates/kernel/src/sched.rs (gauge); слушатель, разметка SDAM и чтение при загрузке — platforms/sdm855/src/persist.rs; блок SDAM и PON — crates/qcom/src/sdam.rs, crates/qcom/src/pon.rs; два watchdog’а — platforms/sdm855/src/watchdog.rs; сборка вердикта — crates/tools/src/crash.rs, crates/tools/src/crash_crumbs.rs; sys.boot — crates/tools/src/sys.rs; имена фаз WLFW — crates/tools/src/wifi/wlfw.rs; живой прогон 18–19.09 — TASKS.md, строка 87. Метки шагов PAS/SMEM/GLINK — platforms/sdm855/src/{remoteproc,ipc}.rs.*
Умный дом под ключ на собственной ОС: 0xd-os и 0xd-uiМы строим умные дома под ключ на компонентах собственной разработки. В основе — ОС 0xd-os, которая одинаково работает на флагманском планшете и в устройстве с 512 КБ памяти.
Зачем агенту безопасности нужна песочницаАгент, который «просто запускает инструменты» на разборе, — второй атакующий, пока у каждого вызова нет гранта. AGI Core изолирует инструменты; Forge держит список скиллов в UIDE.