fix(swap): never touch an unowned swapfile, activate before deactivating [P0]
Two more release blockers in the swap module. 1. Foreign files could be destroyed. swap.path was validated against a blacklist of exact strings. /etc/../etc/fstab is not string-equal to /etc/fstab but resolves to it, and nothing normalized the path or checked the parent chain for symlinks. ensure_desired_swapfile then removed any existing regular file whose size did not match the configured one, so a mistyped or hostile path meant deleting /etc/passwd, an application config, or any other regular file. disable_foreign_swaps additionally deleted foreign swapfiles outright. Ownership is now explicit. A sidecar marker <swap.path>.awg2-harness-owned sits next to the managed swapfile, and inspect_swapfile_ownership classifies the path as Missing, ManagedValid, ManagedWrongSize or Foreign. Foreign is always a hard error - the file is never resized, rewritten or removed. Foreign active swaps are now only swapped off; deleting an administrator's swapfile is not this package's call, and swapoff plus the fstab rewrite already produce the exclusive runtime state. Path validation is split in two. The lexical half rejects relative paths, '.' and '..' components, whitespace, control characters and backslashes, the system path list, and anything under /proc, /sys or /dev; it touches no filesystem, so validate-config stays side-effect free on a host where the file does not exist yet. It runs after '..' is rejected, so the string is already normalized and the blacklist cannot be bypassed. The filesystem half runs just before use and refuses a symlink anywhere in the parent chain, not only at the final component. One deliberate deviation from the audit: arbitrary absolute paths stay allowed rather than being restricted to a fixed managed location. Pinning the path would break deployments that already configured their own, and the ownership marker is the guard that actually prevents data loss - a wrong path can now only fail, never destroy. 2. The migration order left the host swapless on failure. ensure_swap ran disable_foreign_swaps first, then created the swapfile, then activated it. Any failure in fallocate, dd, mkswap or swapon after that first step left the host with no swap at all - on a box provisioned with swap precisely because it is short on memory. The sequence is now a transaction: preflight, prepare the managed swapfile, swapon, verify it actually appears in the active list, and only then swapoff the others, rewrite fstab, apply swappiness and verify. Nothing destructive happens until the replacement is confirmed live. The one unavoidable gap is deactivating our own swap to resize it, which cannot be done in place; foreign swap stays up throughout. chmod 0600 now also happens before mkswap, so the file is never briefly world-readable while it already holds swap structure. config.rs no longer carries its own copy of the path blacklist and calls the shared validator. Twelve tests: the four ownership states, refusal to recreate a foreign file with its content asserted intact afterwards, '..' traversal, dangerous and virtual paths, whitespace and control characters, relative and directory paths, a missing parent directory, an accepted chain, and a symlinked parent (unix only). Docs updated: IMPLEMENTATION_SPEC section 17 gained the ownership table, the ordered transaction and the path rules; CONFIGURATION and the acceptance checklist gained the operator-facing constraints and nine checks. cargo test --workspace: 136 + 4 passed. clippy -D warnings clean.
This commit is contained in:
@@ -535,10 +535,52 @@ Production-путь перезагрузки — внешний бинарь/с
|
||||
- одна запись в `/etc/fstab` с настроенным приоритетом;
|
||||
- `vm.swappiness` из конфига применён в runtime и сохранён persistently.
|
||||
|
||||
При `swap.enabled = true` AWG2 Harness владеет состоянием swap на хосте эксклюзивно. Целевое состояние — ровно одна активная swap-запись: `swap.path`. Все прочие активные swap-устройства и файлы выключаются через `swapoff`. Все не-комментарные swap-записи в `/etc/fstab` удаляются и заменяются одной управляемой записью AWG2. Блочные устройства не удаляются никогда — только выключаются и убираются из fstab. Операция обязана быть идемпотентной.
|
||||
При `swap.enabled = true` AWG2 Harness владеет состоянием swap на хосте эксклюзивно. Целевое состояние — ровно одна активная swap-запись: `swap.path`. Все прочие активные swap выключаются через `swapoff`, а их записи убираются из `/etc/fstab`. Операция обязана быть идемпотентной.
|
||||
|
||||
Создание файла: `fallocate`, при неудаче — fallback на `dd`.
|
||||
|
||||
### Владение swapfile
|
||||
|
||||
Пакет никогда не трогает файл, который не создавал. Рядом с управляемым swapfile лежит sidecar-маркер `<swap.path>.awg2-harness-owned` с первой строкой `# Managed by awg2-harness swap`. Без него отличить наш файл от созданного администратором невозможно, и правило «размер не совпал — пересоздать» превратилось бы в «молча уничтожить чужой файл».
|
||||
|
||||
| Состояние | Условие | Действие |
|
||||
| --- | --- | --- |
|
||||
| `Missing` | файла нет | создать |
|
||||
| `ManagedValid` | маркер есть, размер совпадает | переиспользовать |
|
||||
| `ManagedWrongSize` | маркер есть, размер отличается | пересоздать |
|
||||
| `Foreign` | файл есть, маркера нет | **ошибка**, файл не трогается |
|
||||
|
||||
Чужие активные swap только выключаются через `swapoff`. Файлы не удаляются — ни обычные, ни блочные устройства. Удаление чужого swapfile не является решением этого пакета: эксклюзивное runtime-состояние достигается связкой `swapoff` плюс вычистка `/etc/fstab`, а файл оператор уберёт сам, когда сочтёт нужным.
|
||||
|
||||
### Порядок операций
|
||||
|
||||
Порядок принципиален. Управляемый swap создаётся, активируется и проверяется **до** того, как отключается любой чужой:
|
||||
|
||||
```text
|
||||
1. взять /run/awg2-harness/swap.lock (timeout 30 c)
|
||||
2. preflight: разобрать конфиг, проверить безопасность пути, снять текущее состояние
|
||||
3. подготовить управляемый swapfile (с учётом владения)
|
||||
4. swapon управляемого swap
|
||||
5. убедиться, что он реально появился в списке активных
|
||||
6. только теперь swapoff чужих swap
|
||||
7. перезаписать /etc/fstab транзакцией
|
||||
8. применить swappiness
|
||||
9. проверить итоговое состояние
|
||||
```
|
||||
|
||||
Шаги 2–5 не выполняют ни одного деструктивного действия над чужим состоянием. Прежний порядок отключал чужие swap первым шагом, поэтому ошибка в `fallocate`, `dd`, `mkswap` или `swapon` оставляла хост вообще без swap.
|
||||
|
||||
### Безопасность `swap.path`
|
||||
|
||||
Проверка разделена на две части. Лексическая не обращается к файловой системе и потому пригодна для `validate-config` на хосте, где файла ещё нет:
|
||||
|
||||
- путь абсолютный и не оканчивается на `/`;
|
||||
- нет компонентов `.` и `..` — поэтому строка уже нормализована и список запрещённых путей нельзя обойти записью вида `/etc/../etc/fstab`;
|
||||
- нет пробелов, управляющих символов и обратных слэшей;
|
||||
- путь не входит в список запрещённых (`/`, `/etc`, `/etc/fstab`, `/etc/passwd`, `/etc/shadow`, `/dev`, `/proc`, `/sys`, `/run`, `/boot`) и не лежит под `/proc`, `/sys`, `/dev`.
|
||||
|
||||
Файловая часть выполняется непосредственно перед использованием пути: каждый родительский компонент обязан существовать и не быть symlink. Проверки только финального компонента недостаточно — symlink в середине цепочки перенаправляет операцию на файл, который никогда не настраивался.
|
||||
|
||||
### Перезапись `/etc/fstab`
|
||||
|
||||
Это самая разрушительная операция пакета, поэтому она транзакционная и вынесена в отдельный модуль `fstab.rs`. Контракт:
|
||||
|
||||
Reference in New Issue
Block a user