23 миллисекунды, чтобы солгать

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

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


1. Что было видно

Стадия четвёртая: модем загружен и жив, транспорт GLINK → QRTR → QMI поднят, на нашей стороне запущены серверы, которые прошивка ожидает найти у хоста. Один из них — rmtfs, «дисковый сервер» модема. Через несколько секунд после running модем должен был запустить домен WLAN и объявить сервис WLFW. Вместо этого — fatal.

Слово fatal здесь — не сообщение и не паника. Это бит 0 в слове SMP2P slave-kernel, которое модем выставляет над нашей общей памятью: 6 — здоров, 7 — умер после того, как был готов. Причины смерти модем обычно оставляет в SMEM (SMEM_SSR_REASON_MSS0), но только если успел стартовать собственный сервис ошибок. Здесь слов не было. Правило, которое пришлось выучить раньше: «fatal без причины» всегда значит «раннее» — смерть до того, как модем научился жаловаться.

Что модем делал перед смертью, знали наши серверы. ipc.hosts показывает для каждого хоста, что у него спросили: для rmtfs — какие файлы открыты, сколько секторов прочитано и записано, что отказано и почему. Лог состояний remoteproc даёт время перехода running → crashed. Сложив одно с другим, получили таймлайн: модем открыл /boot/modem_fs1 — ответ есть; /boot/modem_fs2 — есть; /boot/modem_fsg — отказ, no such partition; через 23 мс — fatal. Воспроизводилось каждый раз.

2. Почему модему это важно

EFS — файловая система модема: калибровка радио, IMEI, состояние оператора. Лежит она на разделах диска процессора приложений — нашего диска, до которого модем сам не дотягивается. Поэтому читает и пишет их он через нас, посекторно: OPEN {path} → номер вызывающего, ALLOC_BUFF → адрес общего региона RAM, RW_IOVEC → «перенеси такие‑то сектора туда‑то». Разделов четыре: modemst1 и modemst2 — две рабочие копии, fsc — cookie, fsg — золотая копия, из которой EFS восстанавливается, когда обе рабочие не проходят проверку.

Позже, когда сервер уже работал, стало видно, как модем ими пользуется: по одному сектору со смещением 512 из fs1, fs2 и fsg — заголовки, затем полное чтение выбранной копии. То есть fsg он открывает и заглядывает в неё всегда, при каждом старте, ещё до того как решит, чем пользоваться. Это не аварийный путь — это часть штатной проверки.

На этом планшете раздела fsg нет. В таблице GPT его просто не существует; Samsung, судя по всему, держит золотую копию иначе или не держит вовсе — этого мы не знаем и не утверждаем.

3. Неправильный ответ и правильный

Первая реализация ответила так, как отвечает rmtfs из утилит qrtr, референс для Linux: раздела нет — OPEN отказан. Это честно, это по протоколу, и это ровно тот ответ, который прошивка не переживает. Почему именно отказ на fsg фатален, а отсутствие сервера целиком — нет (без rmtfs модем живёт ещё около 40 секунд и умирает с efs sync failed), мы не знаем: код прошивки закрыт. Наблюдение — только тайминг и воспроизводимость.

Правильный ответ нашёлся из вопроса «что читает стёртая золотая копия?». Нули. Раздел, который никогда не записывали или стёрли, при чтении отдаёт нули, и EFS модема это состояние знает — из нулевой fsg она умеет восстанавливаться (мы это увидели с другой стороны, когда из‑за ошибки кэширования модем прочитал пустые заголовки рабочих копий и восстановил EFS с нуля). Значит, отсутствующий раздел надо открывать пустым: тень из нулей размером с modemst1 (или 2 МиБ, если размер взять не с чего), номер вызывающего как обычно, SUCCESS. Модем читает заголовок, видит стёртую копию, выбирает рабочую — и идёт дальше.

В коде оба ответа остались: Refusal::NoPartition — как делает Linux, и флаг blank_missing, по умолчанию включённый. Комментарий у флага говорит то же: отказ ставит fatal через миллисекунды после OPEN, до того как модем прочитает собственные копии, — так что пусто.

4. Закономерность: хоста проверяют на каждом шаге

Если бы fsg был единственным таким местом, это была бы заметка о квирке. Но тот же рисунок повторяется на каждой стадии между «модем жив» и «Wi‑Fi работает». Прошивка не просто пользуется хостом — она его испытывает, и на неудачный ответ реагирует не ошибкой, а отказом идти дальше или смертью.

Общее у этих шагов: правильность ответа модем проверяет по форме, а не по содержанию. Ему нужен номер вызывающего, а не настоящие сектора; список доменов, а не файл вендора; байты server_check.txt, вернувшиеся такими же, а не директория на диске; код ответа QMI, а не реализация. Отказ, молчание и «нет такого» — единственные ответы, которых он не принимает.

Те же проверки — на одной схеме. Красное — прошивка в роли клиента, зелёное — серверы, которые она ожидает найти у хоста; жирная рамка — где рождается прозрачный ответ: тень, scratch, таблица платы, NOT_SUPPORTED. Сплошная стрелка — механизм, пунктир — что случается, если ответить честно.

Схема: 23 миллисекунды, чтобы солгать

Это фрагмент общей карты жильцов планшета; целиком, с теми же именами узлов, она — в «Чей это компьютер?», §2a.

5. Дизайн, который из этого следует

Отсюда принцип, названный в прошлой заметке прозрачной изоляцией: отвечать на всё, что модем спрашивает, правильным по форме ответом — и не отдавать ничего настоящего. Каждый сервер построен по этому правилу.

Тень вместо диска. Разделы EFS — собственность модема, не наша; сервер получает их только на чтение. При OPEN раздел один раз читается в RAM, и дальше все RW_IOVEC — и чтения, и записи — идут в эту копию. Диск не меняется никогда; перезагрузка забывает. Модем видит, что его записи «легли», и с его точки зрения так и есть. Грязные сектора считаются (dirty в ipc.hosts), но политики сброса их на диск нет — не как флаг, а как код: путь записи отсутствует. Каждый перенос проверяется дважды: сектор и длина против размера тени, адрес в общем регионе против его границ; иначе OutOfRange. Отсутствующий раздел — тень из нулей.

Scratch вместо файлов. tqftp переводит пути модема в свои: всё под /readonly/... — только путь поиска прошивок, имя с .. отбрасывается; всё под /readwrite/... — RAM‑scratch, 64 КиБ и 16 файлов на всё вместе, старые вытесняются. Запись куда‑либо ещё — TFTP‑ошибка ACCESS. server_check.txt пишется в scratch и читается из него же: сервер «проверен», наружу не ушло ни байта.

Пустой список вместо отказа. pd-mapper на вопрос о сервисе, которого нет ни в файлах вендора, ни в таблице платы, отвечает пустым списком доменов — как референс. Это ответ, а не ошибка.

NOT_SUPPORTED вместо молчания. Любое неизвестное сообщение QMI получает корректный ответ с кодом NOT_SUPPORTED; идентификатор записывается в unknown, чтобы было видно, чего модем хотел.

6. Что это стоит

Записи модема в EFS не сохраняются. Всё, что он успел записать за сессию — обновлённое состояние, счётчики, что бы там ни было, — пропадает при перезагрузке. На Linux и Android этого не происходит: rmtfs из qrtr‑утилит и проприетарный rmt_storage пишут в настоящие modemst1/modemst2, tqftpserv пишет в настоящую директорию хоста. Модем получает персистентное хранилище на диске хоста — by design, EFS должна переживать перезагрузку, и по факту переживает даже factory reset: разделы модема ни одна переустановка ОС не трогает.

Для сотового модема это цена: без сохранённого состояния часть его функций работать не будет. Мы её пока платим осознанно — Wi‑Fi через модем работает, а политика записи на диск (если она когда‑нибудь понадобится) записана как долг и потребует владения разделами.

В модели угроз та же цена читается как приобретение. Прошивки Wi‑Fi компрометируют по воздуху регулярно; скомпрометированной прошивке для закрепления нужно куда‑то записаться, и на Linux/Android у неё для этого есть разделы, переживающие всё. У нас ей некуда писать через хоста: тень и scratch живут в RAM, каждая загрузка — чистый лист. Персистентность через хост закрыта не запретом, который можно обойти, а отсутствием пути. Что остаётся — персистентность средствами самого модема, вне нашей видимости, и это надо держать в списке «не закрыто», а не считать решённым.

7. Итог

Двадцать три миллисекунды — это время, за которое прошивка решает, годится ли ей хост. Отказ, пусть и честный, для неё — не информация, а признак неисправного хоста. Отсюда и форма изоляции: не отключать и не запрещать, а на каждом шаге давать ответ, который модем ждёт, — синтаксически правильный и пустой по существу. Тень вместо диска, scratch вместо файлов, пустой список вместо отказа, NOT_SUPPORTED вместо молчания. Чем это является в целом и что остаётся после — в «Паразите в планшете» (docs/wifi-parasite-article.md).


Источники в репозитории: сервер EFS и логика пустой тени — crates/tools/src/rmtfs.rs (документация модуля, Shadow, PATHS, Refusal::NoPartition, blank_missing, тесты); файловый сервер — crates/tools/src/tqftp.rs (Place::of, READWRITE, MAX_SCRATCH_BYTES, MAX_SCRATCH_FILES, тест server_check); карта доменов — crates/tools/src/pdmapper.rs. Транспорт, серверы, тайминги и рукопожатие WLFW — docs/wifi-sm8150.md §4–6; таблица сбросов — там же §7, строки 5 и 6. Что обезврежено и сравнение с Linux — docs/wifi-threat-model.md §3, §6.3, §7. Запись о стадии — TASKS.md, строка 87.

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

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

0xd-ossecurityembeddedwifi
Попробовать