Мегабайт, который убил модем

Опубликовано 2026-09-19
Мегабайт, который убил модем
Серия: Чей это компьютер?Часть 3 из 8

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

01

Проблема

Модем счёл заголовки EFS пустыми, «восстановил» файловую систему с нуля и через 20 секунд упал. Выглядело как ошибка нашего файлового сервера — тень и смещения были валидны.

02

Что сделали

Нашли причину в карте памяти: одолженное модему окно начиналось с середины 2-МиБ блока, и его первый мегабайт остался кэшируемым. Хост писал в кэш, модем читал DRAM напрямую и видел нули. Лечение — одна строка: окно на границу блока. Тем же корнем объяснили ещё три тихих сброса.

03

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

Правило, закреплённое const-проверками в коде: ничто наше не делит ни кэш-линии, ни 2-МиБ блока ни с чем чужим. Три атрибута памяти — три отношения к соседу. Четыре из десяти сбросов были про карту памяти.


0. О чём это

На SoC с сотовым модемом, secure world и гипервизором наша ОС — не единственный хозяин оперативной памяти. Часть RAM мы обязаны одолжить: WLAN‑прошивке — под кольца дескрипторов и буферы DMA, модему — под буфер его файловой системы, TrustZone — под метаданные подписанного образа, который она проверяет. Часть RAM нам не принадлежит вовсе: гипервизор, XBL, AOP, carve‑outы других ядер. И у каждого из этих соседей есть своё представление о том, что лежит по адресу, — представление, в которое наш кэш не входит.

Отсюда то, к чему на макетке с одним процессором привыкнуть негде: атрибуты кэшируемости и выравнивание окон в таблице трансляции — не настройки производительности, а условия корректности и, на этом SoC, безопасности. Мы заплатили за это несколькими тихими сбросами всего чипа; один стоит рассказать подробно, потому что его симптом был так далёк от причины, как только бывает.

1. Симптом: модем чинит то, что мы ему честно отдали

Модем хранит свою постоянную память — EFS — в разделах modemst1, modemst2 и fsg (золотая копия) на нашем диске и ходит к ним через сервер rmtfs, который обязан поднять хост. У нас этот сервер отдаёт модему тень: раздел при OPEN читается в RAM один раз, переносы секторов (RW_IOVEC) идут в копию, диск не меняется. Переносы проходят через общий буфер — 2 МиБ, которые модем просит у хоста командой ALLOC_BUFF и дальше читает своим путём.

Как модем читает EFS, видно по логу сервера: по одному сектору со смещением 512 из fs1, fs2 и fsg — заголовки, — затем полное чтение выбранной копии. В одном из прогонов картина стала такой: заголовки fs1 и fs2 модем счёл пустыми, выбрал fsg (на этом планшете его нет, и он открывается нулями — стёртой золотой копией), восстановил EFS с нуля, записал полные образы в fs1 и fs2 — три полных записи за жизнь, — активировал конфигурацию MCFG и через ~20 с упал с «Modem Graceful Restart failed». (В том же прогоне ядро показало panic: mutex locked pre-entrantly; связь паники с причиной ниже в наших записях не разобрана.)

Всё выглядело как ошибка rmtfs: неверный сектор, неверное смещение, битая тень. Тень оказалась валидной, смещения — верными. Модем действительно видел нули там, куда мы действительно записали заголовки.

2. Причина: мегабайт, который остался кэшируемым

Наш identity‑map устроен просто: L1 по гигабайту, а гигабайт с общими окнами разбит на блоки по 2 МиБ, каждый со своим атрибутом. Окна, в которые пишет или которые читает кто‑то кроме нас, — Normal Non‑cacheable: каждая загрузка и запись идут в память, ни одна наша кэш‑линия там не живёт, обслуживать кэш нечем и незачем. Так отображён SMEM (в Linux — ioremap_wc), так — одолженная RAM.

Одолженная RAM (LENT) на том этапе начиналась с 0x8530_0000. Это не граница блока: 0x8530_0000 — ровно середина блока 0x8520_0000..0x8540_0000. Построение карты идёт по блокам: для каждого блока берётся база и ищется окно, которое её содержит; база 0x8520_0000 ни в одно окно не попадала — и блок остался тем, чем было всё вокруг: Normal write‑back, кэшируемой RAM кучи. Первый мегабайт буфера rmtfs оказался за кэшем, вторая половина — в памяти.

Дальше всё честно с обеих сторон. Хост копировал сектора в буфер обычными записями; буфер считался некэшируемым, поэтому сервер получил «пустую» когерентность (Coherent, ничего не чистить) — и часть записей осела в write‑back‑кэше ядра. Модем читал память своим путём (Q6 → XPU, минуя apps_smmu) и видел то, что лежало в DRAM: нули. Заголовки fs1 и fs2 — в первом мегабайте. Дальше по §1.

Лечение — одна строка: LENT на границу блока. Буфер rmtfs переехал на 0x8520_0000, где и остался в нынешней карте, и прогон с выровненным буфером показал modemst1 r1, modemst2 r2, fsg r1, w0: по одному чтению заголовков, одно чтение копии, ни одной записи.

3. Почему именно 2 МиБ

Потому что таких единиц в нашей карте две: гигабайт и 2 МиБ. Одна таблица L1, одна таблица L2 на тот гигабайт, который позволено делить, никаких страниц по 4 КиБ. Это осознанная бедность: карта строится до кучи, на первых секундах загрузки, и должна быть понятной целиком. Цена — окно отображается точно только если начинается и заканчивается на границе 2 МиБ. Окно, начатое «между», отображается частично как окружение, и делает это молча: в debug‑сборке enable_identity ловит это debug_assert‑ом, в release — отображает как получится.

То же свойство работает в другую сторону. Резервы дерева (no-map) начинаются с hyp_mem по 0x8570_0000 — тоже середина блока. Мы отображаем их Device с 0x8560_0000: мегабайт настоящей RAM отдан, потому что альтернатива — оставить блок гипервизора Normal — хуже (§5.3). «Граница 2 МиБ» — это не только «наше окно целиком некэшируемое», но и «ничего нашего не лежит в одном блоке с чужим».

4. Карта

Адреса — из soc.rs; куча заканчивается там, где начинается одолженная RAM (HEAP_END = LENT.0).

адрес         окно                  атрибут MMU              кто ещё касается
-----------------------------------------------------------------------------------------------
     …        куча                  Normal WB                никто, только мы
0x84e0_0000 +- LENT, 8 МиБ: наша RAM, одолженная другим ---------------------------------------+
            |  LENT_WLAN    4 МиБ    Normal Non-cacheable     copy engines WLAN, DMA по физ.   |
            |                                                 адресам (stage-1 bypass)         |
0x8520_0000 |  LENT_RMTFS   2 МиБ    Normal Non-cacheable     модем: буфер rmtfs, hlos+mss_msa |
0x8540_0000 |  LENT_PAS     2 МиБ    Normal Non-cacheable     secure world: XPU на метаданных  |
0x8560_0000 +- RESERVED: no-map дерева, до 0xa4c0_0000 ---------------------------------------+
            |  отданный МиБ          Device                   никто — цена границы             |
0x8570_0000 |  hyp_mem …             Device                   гипервизор                       |
0x85e0_0000 |  XBL_AOP      2 МиБ    Uncached (вырез)         XBL, AOP; CMD_DB @ 0x85f2_0000   |
0x8600_0000 |  SMEM         2 МиБ    Uncached (вырез)         модем, secure world, гипервизор  |
     …      |  carve-outы ядер       Device                   их ядра, TZ                      |
0x8d80_0000 |  MPSS       160 МиБ    Device; Uncached на      Hexagon модема, TZ               |
            |                        время загрузки образа                                     |
0x9c40_0000 |  SPLASH      32 МиБ    Normal WB (вырез)        дисплей: наш кадр, clean         |
0xa4c0_0000 +----------------------------------------------------------------------------------+

Порядок окон в списке важен — первое окно, содержащее адрес, выигрывает, — поэтому вырезы (SMEM, XBL_AOP, SPLASH) объявлены раньше RESERVED, в который входят. Три атрибута — три отношения к соседу: Normal WB — «здесь только мы»; Non‑cacheable — «сюда пишут и отсюда читают другие, кэш между нами недопустим»; Device — «не наше, процессор не заглядывает сюда без команды».

Таблица отвечает на вопрос «где»; на вопрос «кто и чем» — граф ниже: те же окна, и для каждого — все мастера, которые в него пишут или из него читают, и путь, которым они это делают. Сплошная стрелка — механизм; пунктир — право, которым сосед пользуется, когда сочтёт нужным: чтение метаданных после того, как мы решили, что дело сделано, спекулятивная загрузка через забор, сброс. Жирная рамка — окна этой статьи; полная карта связей всей серии — в «Чей это компьютер?», §2a.

Схема: Мегабайт, который убил модем

5. Три других сброса с тем же корнем

В сводной таблице сбросов у Wi‑Fi‑bring‑up‑а десять строк; четыре из них — про карту памяти. Описанный выше — строка 4. Остальные три.

5.1 Обслуживание кэша по общему окну (строка 1)

Первая версия отображала SMEM кэшируемо и, как положено для DMA‑памяти, чистила и инвалидировала кэш по адресам (dc cvac/dc civac). Через ~10 с после первого remoteproc.list SoC сбрасывался — из secure world, без слова, даже в простое. Доказано двумя контрольными прошивками: без мьютекса и без звонка модему — умирает; без обслуживания кэша — живёт. Точный механизм — чем dc civac по региону, принадлежащему ещё secure world и гипервизору, вызывает сброс, — мы не установили; установили, что референс делает иначе: SMEM у Linux — no-map плюс ioremap_wc, кэша там нет вовсе. Мы сделали так же: SMEM — Non‑cacheable‑окно, qcom::smem получает Coherent (когерентность, которая ничего не делает). Урок шире SMEM: «кэшируемое окно плюс аккуратное обслуживание» для памяти, которую охраняет кто‑то ещё, здесь не эквивалент некэшируемого окна.

5.2 Метаданные PAS в куче (строка 2)

Загрузка модема идёт через PAS: хост отдаёт secure world метаданные образа (init_image), пишет сегменты в carve‑out, вызывает auth_and_reset. Первая версия держала метаданные в Vec в куче — буфер, который после успешного auth_and_reset логично освободить. Сброс приходил через 6–10 с после старта модема, при простаивающем процессоре приложений, без меток.

Причина: TrustZone ставит на метаданные XPU и читает их ещё во время авторизации сегментов и после возврата auth_and_reset — секунды после того, как хост счёл дело сделанным. Освобождённый буфер куча переиспользовала; запись в него стала записью в защищённую страницу — и сбросом. В референсе след того же: qcom_scm_pas_metadata_release вызывается после auth (с Linux 5.18), не раньше.

Лечение — своё окно: LENT_PAS, 2 МиБ, Non‑cacheable, вне кучи. Метаданные копируются туда, secure world получает этот адрес, и до завершения загрузки никто больше в это окно не пишет. Общее с §2: память, которую читает другой мастер, не лежит ни там, где её переиспользуют, ни за нашим кэшем.

5.3 Спекуляция через забор (строка 10)

Эта строка — единственная в таблице с пометкой «не наблюдён, закрыт превентивно», и мы это честно повторяем. Normal‑маппинг — даже Non‑cacheable — разрешает процессору спекулятивные чтения: ядро может выполнить загрузку вперёд программы, по адресу, куда программа, возможно, и не пойдёт. Если этот адрес — за XPU‑забором secure world или за stage‑2 гипервизора, ответ на такое чтение — bus error, а на этом SoC bus error из обычного мира означает сброс в момент, который выбирает не программа. Отсюда правило: всё, что дерево помечает no-map, для нас — Device, память, которую процессор не читает вперёд. И отсюда же отданный мегабайт перед hyp_mem: блок гипервизора не должен быть Normal ни на байт.

У правила есть динамическая половина. Carve‑out модема (MPSS, 160 МиБ) с загрузки Device: не наш. На время записи образа он открывается Non‑cacheable (remap на работающем MMU, инвалидация TLB широковещательная), сегменты пишутся прямо в память, после auth_and_reset окно закрывается обратно в Device. Закрыть можно только потому, что открывали Non‑cacheable: у окна нет грязных линий, которые пришлось бы вымывать перед сменой атрибута. Условия remap те же, что у всей карты: границы 2 МиБ, ничего нашего внутри.

6. Инвариант и как он проверяется при компиляции

После четырёх сбросов правило сформулировалось коротко: ничто наше не делит ни кэш‑линии, ни 2‑МиБ‑блока ни с чем чужим. Развёрнуто:

  1. Память, которую пишет или читает другой мастер (модем, WLAN‑таргет, secure world), — Normal Non‑cacheable, в собственном окне, вне кучи.
  2. Память, которая нам не принадлежит (no-map), — Device; открывается Non‑cacheable только на время, пока мы в неё пишем, и закрывается обратно.
  3. Каждое окно начинается и заканчивается на границе 2 МиБ.
  4. Куча заканчивается там, где начинается первое одолженное окно.

Пункты 3 и 4 проверяются до запуска, компилятором. В soc.rs рядом с константами карты стоит блок const _: () = { … }: макрос aligned! для каждого окна — SMEM, LENT, LENT_WLAN, LENT_RMTFS, LENT_PAS, RESERVED, XBL_AOP, SPLASH — утверждает, что база и длина кратны 2 << 20; отдельный assert! — что три окна LENT ровно заполняют её. Сдвинуть окно на мегабайт, как в §2, теперь означает не собрать образ, а не получить тихий сброс через двадцать секунд после старта модема. Куча упирается в LENT.0 тоже константой (HEAP_END), и разойтись с картой не может.

Остальное — проверки во время выполнения, потому что данных нет раньше. enable_identity в debug‑сборке проверяет выравнивание окон ещё раз, независимо от soc.rs. Перед тем как что‑либо одолжить, платформа смотрит, куда загрузчик положил дерево устройств: если оно легло внутрь LENT, не одалживается ничего — ни буфер rmtfs, ни метаданные PAS, ни кольца WLAN, — потому что дерево читается всю жизнь машины и делить с ним окно, куда пишет модем, нельзя. remap отказывает на невыровненном или чужом окне, ничего не меняя.

Методически важно: const‑ассерты появились как лекарство от конкретного сброса, а закреплены как требование безопасности — первым пунктом самого дешёвого уровня в модели угроз. Требование, в отличие от лекарства, не удалят при рефакторинге как лишнюю строку.

7. Что это значит для модели угроз

В «Паразите» граница с жильцом описана через каналы: DMA, rmtfs, tqftp, memshare, SMEM. Карта памяти — не ещё один канал, а подложка под все: каждый канал — окно в RAM, и свойства окна — атрибуты, границы, кто ещё туда пишет — определяют, что жилец в нём увидит и чего не увидит.

Случай из §1 показателен: модем не атаковал нас. Он получил от хоста не те данные, что мы записали, интерпретировал их по своим правилам и предпринял действие с необратимыми последствиями для собственного состояния. Будь EFS на настоящем диске, как у Linux и Android, три полных перезаписи fs1/fs2 образами, восстановленными из пустой золотой копии, легли бы на диск. Тень спасла диск; но источник был не в rmtfs, а в поле атрибутов дескриптора трансляции.

Отсюда два следствия для границы с паразитом:

Это не заменяет главного — трансляции DMA через SMMU для потока 0x640, о которой шла речь в «Паразите». Но без правильной карты трансляция защищает окно, содержимое которого мы сами не в состоянии корректно предъявить.

8. Выводы

  1. На SoC с другими мастерами и secure world кэшируемость и выравнивание — не ручки производительности. Кэшируемый буфер, который читает другой мастер, — буфер с двумя разными содержимыми; невыровненное окно — окно с двумя разными атрибутами.
  2. Три атрибута — три отношения к соседу: Normal WB «только мы», Non‑cacheable «мы и они, без кэша между», Device «только они, мы не заглядываем». Смена отношения — смена атрибута, а не дисциплина обслуживания.
  3. Правило одно: ничто наше не делит ни линии, ни блока ни с чем чужим. Оно стоило четырёх сбросов из десяти, и один выглядел как ошибка файлового сервера.
  4. Такие инварианты надо делать проверяемыми до запуска. Константы карты и const‑ассерты рядом с ними — самый дешёвый забор у этой ОС и единственный, который не может тихо перезагрузить планшет.
  5. Карта памяти — часть границы с жильцом из «Паразита в планшете»: подраздел «Кэш — ничего общего» в его §7 — не одна мера среди прочих, а основание, на котором остальные стоят.

Откуда факты. Карта и комментарии к ней — platforms/sdm855/src/soc.rs (LENT, LENT_WLAN, LENT_RMTFS, LENT_PAS, RESERVED, XBL_AOP, SMEM, SPLASH, блок const _: () = { … aligned!(…) }); порядок окон и HEAP_ENDplatforms/sdm855/src/main.rs, platform.rs. Построение карты, атрибуты, remap, clean_dcache/invalidate_dcachecrates/aarch64/src/mmu.rs. Carve‑out, метаданные PAS, lent_is_freeplatforms/sdm855/src/remoteproc.rs (Identity). Правила MMU, таблица сбросов (строки 1, 2, 4, 10), картина чтения EFS — docs/wifi-sm8150.md §2, §7, §5.3. Модель угроз — docs/wifi-threat-model.md §3 (абзац об MMU), §5 уровень 1 п. 1. Живой прогон — TASKS.md, строка 87. Оговорка: §2 docs/wifi-sm8150.md описывает более раннюю карту (LENT с 0x8520_0000, 4 МиБ, без LENT_WLAN); адреса здесь — по soc.rs, где LENT — 8 МиБ с 0x84e0_0000, а буфер rmtfs остался по 0x8520_0000.*

Что это значит для вас
Кэш — это канал, блок — единица доверия: выравнивание и кэшируемость — не ручки производительности, а условия безопасности рядом с чужим кодом. Будь EFS на настоящем диске, как у Android, три полных перезаписи модема легли бы на него навсегда. Это уровень, на котором мы разбираем устройства — и на котором проектируем свои.

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

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

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

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