feat: refuse to install over a partition which is in use - #434
Open
otavio wants to merge 1 commit into
Open
Conversation
Nothing stopped an update from writing over storage the running system still had mounted. The requirements check only asked whether the target existed and was writable, so a raw object pointed at the live rootfs was written straight through the block device, behind the back of a filesystem which went on serving its own page cache and writing it back over what had been installed. Every handler reaching the storage directly is affected, and imxkobs validated nothing at all, not even that the chips it writes to exist. The two which go through a filesystem, copy and tarball, are fine while they only write files, but both format the target first when asked to, and utils::fs::format passes -F, -f or -y so the mkfs tools run unattended, which also means they will not refuse a mounted device on their own. ensure_not_mounted() reads /proc/self/mountinfo and rejects a target backing a mounted filesystem. Devices are compared by number rather than by path, so the by-uuid link, /dev/root and the device mapper name all resolve to the same device, and sysfs extends the comparison to the storage the target shares: the partitions a disk holds, the disk a partition belongs to, and whatever is stacked on top through LVM, MD or loop. Sibling partitions are left out, as writing to one does not reach the others. UBIFS and JFFS2 mount no block device, so there is no number to compare and mountinfo carries a name instead, as ubi0:rootfs or mtd:rootfs. Those are rebuilt from /sys/class/mtd and /sys/class/ubi, which also covers erasing an MTD out from under the UBI volumes attached to it. Rather than let a handler ask for the check separately, and leave the next one free to forget it, valid() now takes how the handler reaches its target. Every handler already calls it, so each is made to say whether it writes the device itself or goes through a filesystem it mounts, and the safe answer is no longer the one you have to remember to ask for. copy and tarball answer with whichever applies, as formatting takes the device for itself even though the rest of their work does not. The whole package is therefore rejected during validation, before anything is downloaded and before an earlier object has written to storage. format() keeps a check of its own, so the mkfs calls stay guarded no matter who reaches them. The imxkobs tests named /dev/sda1 and /dev/sda2 as stand-ins for chips they never touch, which the check now rejects on any host where those are mounted, so they name devices which do not exist instead.
otavio
force-pushed
the
protect-in-use-partitions
branch
from
July 28, 2026 21:05
bc75fbc to
50421e3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #353.
An update could write over storage the running system still had mounted.
TargetTypeExt::valid()only checked that the target existed and was writable, so arawobject pointed at the live rootfs went straight through the block device while the mounted filesystem kept serving its own page cache and writing it back over what had just been installed.Audit of the handlers
The issue asked to double-check every mode, so here is where each stood:
valid(Access::Exclusive)valid(Access::Exclusive)valid(Access::Exclusive)valid(Access::Exclusive)kobs-ngon PATH, no target validation whatsoevervalid(Access::for_format(should_format))valid(Access::for_format(should_format))How the check works
utils::fs::ensure_not_mounted()reads/proc/self/mountinfoand matches on two axes:rdevis compared against mountinfo'smajor:minor, so the by-uuid link,/dev/rootand a device mapper name all resolve to the same device. sysfs then extends the comparison to storage the target shares: the partitions a disk holds, the disk a partition belongs to, and whatever is stacked on top through LVM, MD or loop. Sibling partitions are deliberately excluded, since writing to one does not reach the others.ubi0:rootfsormtd:rootfs. Those names are rebuilt from/sys/class/mtdand/sys/class/ubi, which also catches erasing an MTD out from under the UBI volumes attached to it.Where the policy lives
valid()now takes anAccessargument saying how the handler reaches its target —Exclusivefor handlers writing the device itself,ThroughFilesystemfor those writing through a mount they make. Every handler already calledvalid(), so each is forced by the compiler to state its intent exactly once, and there is no separate call for the next handler to forget.copyandtarballpassAccess::for_format(should_format): formatting rewrites the whole filesystem and so takes the device for itself, even though the rest of their work is safe on an already-mounted target. That also means the whole package is rejected during validation — before anything is downloaded, and before an earlier object in the same package has written to storage.format()keeps a check of its own, so the mkfs calls stay guarded regardless of who reaches them. The force flags there (-F/-f/-y) are deliberately kept: dropping them would make the tools prompt on a whole disk or an existing filesystem, and an unattended agent with no tty would hang or abort. The explicit check is stronger anyway, since mkfs's own refusal covers only the exact device path, not partitions or holders.Verification
cargo test --all --all-featurespasses, clippy is clean.Probed against real devices on a development host:
The
/dev/sda2case surfaced on its own: the imxkobs fixture used/dev/sda1and/dev/sda2as placeholders and the new check rejected them, because/dev/sda2really is a LUKS PV on that host. The fixture now names devices which do not exist, which also exercises the not-found path where a missing device returnsOkand leaves the existence complaint tovalid().Tests cover mountinfo parsing with and without the optional fields, malformed lines, a regular file and a missing device. A loop-device round trip is included as
#[ignore]d, following theloopdevpattern thecopy/tarballtests already use and sharing theirSERIALIZEmutex; note CI runscargo testwithout--ignored, so it documents intent rather than running in the pipeline.Known limitations
mount_sources_forenumerates the spellings a kernel might have recorded for an MTD or UBI target rather than resolving each mountinfo source forward to a canonical device. A spelling that is missing fails open. UBIFS superblocks carry anonymous devnos so some name-based reasoning is unavoidable, but the direction of the mapping could be inverted later, using the fstype field the parser already walks past.