Заметки с bring‑up‑а собственной ОС на Samsung Galaxy Tab S6 (SM‑T865, Snapdragon 855). Продолжение «Паразита в планшете»: там — кто такой жилец с ключами от дома; здесь — про одну стену между нами, которую мы поставили на метр левее, чем нужно, и что из этого вышло.
Проблема
Модем счёл заголовки EFS пустыми, «восстановил» файловую систему с нуля и через 20 секунд упал. Выглядело как ошибка нашего файлового сервера — тень и смещения были валидны.
Что сделали
Нашли причину в карте памяти: одолженное модему окно начиналось с середины 2-МиБ блока, и его первый мегабайт остался кэшируемым. Хост писал в кэш, модем читал DRAM напрямую и видел нули. Лечение — одна строка: окно на границу блока. Тем же корнем объяснили ещё три тихих сброса.
Что получилось
Правило, закреплённое 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‑МиБ‑блока ни с чем чужим. Развёрнуто:
- Память, которую пишет или читает другой мастер (модем, WLAN‑таргет, secure world), — Normal Non‑cacheable, в собственном окне, вне кучи.
- Память, которая нам не принадлежит (
no-map), — Device; открывается Non‑cacheable только на время, пока мы в неё пишем, и закрывается обратно. - Каждое окно начинается и заканчивается на границе 2 МиБ.
- Куча заканчивается там, где начинается первое одолженное окно.
Пункты 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, а в поле атрибутов дескриптора трансляции.
Отсюда два следствия для границы с паразитом:
- Кэш — это канал. Что мы держим в кэше и считаем записанным, для соседа не существует, пока не дошло до DRAM; что сосед записал в DRAM, для нас не существует, пока мы читаем из кэша. Некэшируемое окно — единственный способ, чтобы обе стороны видели одну память, не полагаясь на дисциплину обслуживания, которую здесь к тому же наказывают сбросом (§5.1).
- Блок — это единица доверия. Если окно жильца и наши данные оказались в одном блоке, атрибут у них один, и один из двух получил не тот. Совпадение границ карты с границами одолженного — часть определения «нашего» и «чужого», а не деталь реализации.
Это не заменяет главного — трансляции DMA через SMMU для потока 0x640, о которой шла речь в «Паразите». Но без правильной карты трансляция защищает окно, содержимое которого мы сами не в состоянии корректно предъявить.
8. Выводы
- На SoC с другими мастерами и secure world кэшируемость и выравнивание — не ручки производительности. Кэшируемый буфер, который читает другой мастер, — буфер с двумя разными содержимыми; невыровненное окно — окно с двумя разными атрибутами.
- Три атрибута — три отношения к соседу: Normal WB «только мы», Non‑cacheable «мы и они, без кэша между», Device «только они, мы не заглядываем». Смена отношения — смена атрибута, а не дисциплина обслуживания.
- Правило одно: ничто наше не делит ни линии, ни блока ни с чем чужим. Оно стоило четырёх сбросов из десяти, и один выглядел как ошибка файлового сервера.
- Такие инварианты надо делать проверяемыми до запуска. Константы карты и
const‑ассерты рядом с ними — самый дешёвый забор у этой ОС и единственный, который не может тихо перезагрузить планшет. - Карта памяти — часть границы с жильцом из «Паразита в планшете»: подраздел «Кэш — ничего общего» в его §7 — не одна мера среди прочих, а основание, на котором остальные стоят.
Откуда факты. Карта и комментарии к ней — platforms/sdm855/src/soc.rs (LENT, LENT_WLAN, LENT_RMTFS, LENT_PAS, RESERVED, XBL_AOP, SMEM, SPLASH, блок const _: () = { … aligned!(…) }); порядок окон и HEAP_END — platforms/sdm855/src/main.rs, platform.rs. Построение карты, атрибуты, remap, clean_dcache/invalidate_dcache — crates/aarch64/src/mmu.rs. Carve‑out, метаданные PAS, lent_is_free — platforms/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. Напишите, что у вас.
Умный дом под ключ на собственной ОС: 0xd-os и 0xd-uiМы строим умные дома под ключ на компонентах собственной разработки. В основе — ОС 0xd-os, которая одинаково работает на флагманском планшете и в устройстве с 512 КБ памяти.
Зачем агенту безопасности нужна песочницаАгент, который «просто запускает инструменты» на разборе, — второй атакующий, пока у каждого вызова нет гранта. AGI Core изолирует инструменты; Forge держит список скиллов в UIDE.