firewall guard: барьер покоя fail-closed и явный контракт транзиентного таймера
Барьер, обязанный ДОКАЗАТЬ отсутствие асинхронного исполнителя, в трёх местах принимал за доказательство отсутствие наблюдения. - отказ `systemctl` больше не выдаётся за отсутствие guard: вместо `return []` введён единый наблюдатель inspectRollbackGuard с исходами quiescent/pending/ unknown и отдельным типом отказа GuardStateUnknownError; - покой перечисляется белым списком (inactive, failed): maintenance, refreshing и любое незнакомое состояние systemd блокируют операцию; - у транзиентного таймера явно заданы AccuracySec=1s (умолчание 1min превращало обещанные 45 секунд в 45-105) и RemainAfterElapse=no; барьер дополнительно опознаёт SubState=elapsed у *.timer как покой; - команда взведения строится чистой buildArmGuardArgv и выполняется новым runMutatingArgv без shell, поэтому её контракт проверяется значением, а не грепом по исходнику; - status перестал листить guard-юниты своей копией кода: без --plain, с `|| true` и с трактовкой failed как «вооружён» отчёт вечно противоречил барьеру. Добавлены rollback_guard_state и firewall_state=guard_unknown; - purge-v0.sh пропускал failed-юниты из-за маркера в первой колонке. Барьер покрыт поведенческими тестами через подставляемый SystemdUnitProbe: прежние проверки грепом по тексту функции пережили инверсию смысла - строка `return [];` была на месте, а решение стало неверным. Документация (README, docs/07, 11, 12, 13, 14, CHANGELOG) приведена к реальному окну 45-46 секунд и к новому тексту отказа. Отдельно исправлен комментарий PNPM_AUDIT_LEVEL в versions.env: гейт давно проверяет весь lock-граф.
This commit is contained in:
@@ -37,6 +37,87 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
||||
допущения, на которых держалась операция целиком: что снятие защиты от отката
|
||||
действительно произошло и что операция на сервере ровно одна.
|
||||
|
||||
Седьмой проход — приведение барьера покоя к реальной семантике systemd 257,
|
||||
который стоит на Debian 13. Механизм, обязанный **доказать** отсутствие
|
||||
асинхронного исполнителя, в трёх местах принимал за доказательство отсутствие
|
||||
наблюдения.
|
||||
|
||||
### Исправлено — барьер покоя и контракт транзиентного таймера
|
||||
|
||||
- **Отказ запроса к systemd выдавался за отсутствие guard'а.** Листинг
|
||||
guard-юнитов при исключении возвращал пустой список, и пустой список означал
|
||||
«можно начинать»:
|
||||
|
||||
```text
|
||||
systemd жив, старый rollback timer взведён
|
||||
-> запрос к systemctl/D-Bus временно отказывает
|
||||
-> список guard'ов пуст
|
||||
-> барьер считает систему спокойной
|
||||
-> новая операция начинает менять firewall
|
||||
-> старый таймер срабатывает поверх неё
|
||||
```
|
||||
|
||||
Обоснование в комментарии («без systemd не может быть и транзиентного
|
||||
таймера») доказывало не то: отказ запроса не означает, что systemd нет.
|
||||
Барьер стал fail-closed и получил отдельный тип отказа
|
||||
`GuardStateUnknownError` — «guard вооружён» и «спросить не удалось» требуют
|
||||
от оператора разных действий. Практического выигрыша у прежнего поведения не
|
||||
было: `systemd-run` требуется в preflight, поэтому без работающего systemd
|
||||
операция всё равно отказывала — просто позже и менее внятно.
|
||||
|
||||
- **Покой перечислялся чёрным списком.** Непокойными считались `active`,
|
||||
`activating`, `deactivating`, `reloading`, а покоем — «всё остальное», то
|
||||
есть любое состояние, которого автор не назвал. systemd 257 знает ещё
|
||||
`maintenance` и `refreshing`. Политика инвертирована: покой — это `inactive`
|
||||
и `failed`, всё прочее блокирует операцию.
|
||||
|
||||
- **Обещанные «45 секунд» не были контрактом systemd.** `OnActiveSec=45s` не
|
||||
означает «ровно через 45 секунд»: таймер вправе сработать в окне
|
||||
`[цель; цель + AccuracySec]`, а умолчание `AccuracySec=` — одна минута. То
|
||||
есть README, docs и текст отказа обещали 45 секунд, а контракт допускал 105.
|
||||
Теперь точность задаётся явно (`AccuracySec=1s`), и реальное окно — 45–46
|
||||
секунд.
|
||||
|
||||
- **Выгрузка отработавшего таймера держалась на чужом умолчании.** Право
|
||||
барьера считать исчезновение юнита покоем опирается на
|
||||
`RemainAfterElapse=no`. `systemd-run` выставляет это свойство транзиентным
|
||||
таймерам сам, но инвариант, который нигде не записан и ничем не проверяется,
|
||||
инвариантом не является. Свойство задаётся явно, а барьер дополнительно
|
||||
опознаёт `SubState=elapsed` у `*.timer` как покой: `TIMER_ELAPSED` в systemd
|
||||
отображается в `UNIT_ACTIVE`, и без этой ветки отработавший таймер,
|
||||
созданный не нами, блокировал бы `repair` навсегда.
|
||||
|
||||
- **`status` расходился с барьером в трактовке того же самого guard'а.** У него
|
||||
была своя копия листинга — без `--plain` (у `failed`-юнита первой колонкой
|
||||
идёт маркер `●`), с `|| true` (отказ systemd превращался в «guard'ов нет») и
|
||||
без разбора состояний: вооружённым считался любой найденный юнит. Аварийно
|
||||
сработавший guard оставляет `failed`-сервис загруженным до `reset-failed`,
|
||||
поэтому `status` вечно показывал `firewall_state: guard_active`, пока барьер
|
||||
тот же юнит считал покоем и разрешал `repair`. Копия убрана: отчёт берёт
|
||||
состояние у барьерного наблюдателя и сообщает `rollback_guard_state`
|
||||
(`quiescent` / `pending` / `unknown`) с `active_state` и `sub_state` каждого
|
||||
юнита.
|
||||
|
||||
- **`purge-v0.sh` не снимал именно аварийно сработавший guard.** Разбор
|
||||
`awk '{print $1}' | grep -E '^hy2xs-fw-rollback-'` отбрасывал строку целиком,
|
||||
когда первой колонкой стоял маркер `●`, то есть пропускал `failed`-юниты —
|
||||
единственные, ради которых эта проверка написана.
|
||||
|
||||
- **Контракт команды взведения проверялся грепом по исходнику.** Команда
|
||||
собиралась интерполяцией в shell-строку и существовала только в момент
|
||||
запуска. Теперь её строит чистая `buildArmGuardArgv`, а выполняет
|
||||
`runMutatingArgv` — без shell вообще, что заодно убирает вопрос о
|
||||
квотировании из команды, создающей systemd-юнит с именем из данных операции.
|
||||
Свойства таймера стали обычным значением, которое сравнивает обычный тест.
|
||||
|
||||
- **Барьер не имел ни одного поведенческого теста.** Все проверки были грепами
|
||||
по тексту функций, и один из них закреплял как раз небезопасное поведение:
|
||||
тест утверждал, что в теле есть `return [];`. Строка была на месте — а
|
||||
решение при этом стало неверным. Наблюдение за systemd вынесено в
|
||||
подставляемый `SystemdUnitProbe`, и сценарии («systemd не ответил», «таймер
|
||||
взведён», «сервис упал», «незнакомое состояние», «таймер отработал»)
|
||||
проверяются поведением, без systemd и без Linux.
|
||||
|
||||
### Исправлено — границы транзакции
|
||||
|
||||
- **Снятие rollback guard было утверждением, а не фактом.** Порядок фиксации
|
||||
|
||||
Reference in New Issue
Block a user