fix(swap): bind ownership to device+inode, restrict swap.path to root-owned roots [P0]
Two defects that together made the previous ownership model unsafe, and reverse an earlier deviation that the follow-up audit correctly rejected. 1. The sidecar marker identified a path, not a file. Ownership was `<swap.path>.awg2-harness-owned` sitting next to the swapfile, and the check was whether that sidecar existed and started with our marker. Nothing tied it to the file it was supposed to describe. The sequence that loses data: our swapfile is removed by hand, the sidecar survives, an operator puts an unrelated file at the same path - and the next reconcile calls it managed and, if the size differs, deletes it. The claim in the previous commit that a wrong path "can now only fail, never destroy" was wrong. Ownership now lives in /var/lib/awg2-harness/swap-ownership.conf, a root-only directory created 0700, and records version, path, device and inode. A path is not an identity: a file can be deleted and another created in its place, but (device, inode) cannot be reused while our file exists. Classification gained StaleOwnership for "right path, different file", which refuses exactly like Foreign but says which situation the operator is actually in. Only ManagedValid and ManagedWrongSize ever authorise a delete, and that is asserted directly. The record is written after the file exists, from its real metadata, so until it lands the swapfile is not considered managed. The old sidecar is now only ever removed, never read. classify_ownership is a pure function taking the recorded state and the observed identity, so the cases that matter - stale record, record for another path, no record - are unit-testable rather than requiring a Linux filesystem. 2. An arbitrary swap.path was still exploitable. The previous commit kept arbitrary absolute paths and argued the ownership marker was sufficient protection. It is not, and the audit is right to reject it. Between assert_swap_path_is_safe and fallocate/dd/mkswap running, a user who can write the parent directory replaces the final component with a symlink and redirects a root-owned operation. That is a TOCTOU; stat-ing before use cannot close it, because the check and the use are separate syscalls. Paths like /tmp/awg.swap or /home/user/awg.swap made this reachable by any local user. swap.path is now restricted to /swapfile or a file directly under /var/lib/awg2-harness/. Both are root-owned, so the window does not exist rather than being detected. /swapfile is kept so existing deployments need no config change. The deny-list it replaces only ruled out the paths someone had thought of; what actually matters is who can write the parent. Eleven tests: the five classification states, a stale record, a record for another path, the invariant that no refusal state is managed, record round-trip, malformed and unsupported-version records, the newly allowed and newly rejected path shapes. cargo test --workspace: 176 + 4 passed. clippy -D warnings clean.
This commit is contained in:
@@ -198,9 +198,11 @@ swappiness = 1
|
||||
|
||||
`swap.path`, `swap.size_mb`, `swap.priority`, `swap.swappiness` — параметры managed swap. При `swap.enabled=true` пакет приводит swap-state к exclusive состоянию: активен ровно один swap (`swap.path`), остальные выключаются и swap-entries в `/etc/fstab` пересобираются в одну managed строку. Рекомендуемый low priority — `1`, `swappiness = 1`.
|
||||
|
||||
Ограничения `swap.path`: абсолютный, без компонентов `.` и `..`, без пробелов, управляющих символов и обратных слэшей, не из списка системных путей (`/`, `/etc`, `/etc/fstab`, `/etc/passwd`, `/etc/shadow`, `/dev`, `/proc`, `/sys`, `/run`, `/boot`) и не под `/proc`, `/sys`, `/dev`. Дополнительно на этапе применения проверяется, что ни один родительский каталог не является symlink.
|
||||
Ограничения `swap.path`: путь должен быть либо `/swapfile`, либо файлом непосредственно в `/var/lib/awg2-harness/`. Дополнительно: абсолютный, без компонентов `.` и `..`, без пробелов, управляющих символов и обратных слэшей; на этапе применения проверяется, что ни один родительский каталог не является symlink.
|
||||
|
||||
Пакет создаёт рядом с swapfile маркер владения `<swap.path>.awg2-harness-owned`. Файл без этого маркера считается чужим: он никогда не пересоздаётся и не удаляется, вместо этого возвращается ошибка. Чужие активные swap только выключаются — их файлы пакет не удаляет.
|
||||
Ограничение корней обязательно, а не косметика. Если родительский каталог доступен на запись кому-то кроме root, между проверкой пути и запуском `fallocate`/`mkswap` этот пользователь успевает подменить файл на symlink и перенаправить операцию, выполняемую от root. Оба разрешённых корня принадлежат root, поэтому окна не возникает. `/swapfile` оставлен для совместимости с уже развёрнутыми конфигурациями.
|
||||
|
||||
Владение фиксируется в `/var/lib/awg2-harness/swap-ownership.conf` по `device` + `inode` + пути, а не по маркеру рядом с файлом. Файл, которого нет в этой записи, или файл с тем же путём, но другими `device`/`inode` (то есть подменённый), считается чужим: он никогда не пересоздаётся и не удаляется, вместо этого возвращается ошибка. Чужие активные swap только выключаются — их файлы пакет не удаляет.
|
||||
|
||||
## Что не выносить в конфиг
|
||||
|
||||
|
||||
Reference in New Issue
Block a user