Skip to content

fix: measure the install target's own capacity, not the devtmpfs holding its node - #433

Open
otavio wants to merge 1 commit into
masterfrom
fix/disk-space-check-measures-devtmpfs
Open

fix: measure the install target's own capacity, not the devtmpfs holding its node#433
otavio wants to merge 1 commit into
masterfrom
fix/disk-space-check-measures-devtmpfs

Conversation

@otavio

@otavio otavio commented Jul 28, 2026

Copy link
Copy Markdown
Member

Closes #409.

The bug

ensure_disk_space() checked free space with statvfs(target), but every caller passes a device node/dev/mmcblk0p2 for raw/copy, an MTD character device for flash, a UBI volume for ubifs. statvfs reports on the filesystem holding the path it is given, and for anything under /dev that is the devtmpfs the kernel sizes after available RAM. The check never once looked at the partition being written to.

Measured on a development machine — three very different devices, one identical number, and that number is df /dev:

                 lseek END        statvfs available
/dev/nvme0n1     2000398934016    3365040128
/dev/nvme0n1p1     535822336      3365040128
/dev/sda         15610576896      3365040128

$ df -h /dev
devtmpfs  3.2G  0  3.2G  0%  /dev

This confirms the diagnosis @je-s reached in #409. It also explains why the reporter only hit it after growing their partitions: the rootfs used to be smaller than devtmpfs, so the meaningless comparison happened to pass. At a 10 GiB required_uncompressed_size it no longer did.

The failure is symmetric, and the other direction is the more dangerous one: a device too small for the update is accepted whenever RAM happens to exceed the object size.

The fix

Device nodes are asked for their own capacity by seeking to the end, which block devices, MTD character devices and UBI volumes all answer with their size. That is portable in a way the ioctls are not — BLKGETSIZE64 is declared in terms of size_t, so its request number differs between 32- and 64-bit userspace while the kernel writes back 64 bits either way, and MTD and UBI would each need an ioctl of their own besides. Paths that really do live on a filesystem keep going through statvfs.

A second fix on the same line: the statvfs fields are c_ulong/fsblkcnt_t, 32-bit on the armv7 targets this runs on, so the product wrapped for any filesystem past 4 GiB and the u64 conversion came too late to help. Both operands are widened first now, and the count is taken in units of f_frsize rather than f_bsize, which is what f_bfree is expressed in.

Tests

Two unit tests cover the statvfs path and the rejection behaviour. A third sets up a loop device and asserts the reported capacity is the device's own 64 MiB and not devtmpfs's free space — it needs root, as creating a loop device does, so it is marked #[ignore] alongside the MTD tests that already are. Run under sudo:

test utils::fs::tests::device_nodes_are_measured_by_their_own_capacity ... ok

cargo fmt --check, cargo clippy --all-features --all --tests and the full suite (157 passed) are clean.

Left for later

copy and tarball mount a filesystem onto the target and write files into it, so the device's capacity is an upper bound for them rather than an answer — it ignores both what is already there and the filesystem's own overhead. Measuring those properly means a statvfs on the mount point once mounted, which is a change of its own. They are still far better off than measuring devtmpfs.

…ing its node

ensure_disk_space() asked statvfs about the install target, but every caller
hands it a device node: /dev/mmcblk0p2 for raw and copy, an mtd character
device for flash, a ubi volume for ubifs. statvfs reports on the filesystem
holding the path it is given, and for anything under /dev that is the devtmpfs
the kernel sizes after the available RAM. The check therefore compared the
object against a fraction of RAM and never once looked at the partition the
object was about to be written to.

The same number came back for every device on a machine here, 3.2G, which is
what df reports for /dev, while the devices themselves are 1.8T, 511M and
14.5G. A device large enough for the update was rejected, and, worse, one too
small was accepted whenever RAM happened to exceed it.

Device nodes are now asked for their own capacity by seeking to the end, which
block devices, mtd character devices and ubi volumes all answer with their
size. That is portable in a way the ioctls are not: BLKGETSIZE64 is declared
in terms of size_t, so its request number differs between 32 and 64 bit
userspace while the kernel writes back 64 bits either way, and mtd and ubi
would each need an ioctl of their own besides. Paths that really do live on a
filesystem keep going through statvfs.

Widening the statvfs fields before multiplying fixes a second way the same
line could mislead. They are c_ulong and fsblkcnt_t, 32 bit on the armv7
targets this runs on, so the product wrapped for any filesystem past 4GiB and
the conversion to u64 came too late to help. The count is also taken in units
of f_frsize rather than f_bsize, which is what f_bfree is expressed in.

The loop device test needs root, as creating one does, so it is marked ignored
alongside the mtd tests that already are. Its fixture is built on the loopdev
crate and on the mutex the copy and tarball tests already serialize next_free()
with, both of which had to be reachable from here, rather than on a second way
of doing what the crate can already do.

Left alone: copy and tarball mount a filesystem onto the target and write
files into it, so the device's capacity is an upper bound for them rather than
an answer, ignoring both what is already there and the filesystem's own
overhead. Measuring those properly means a statvfs on the mount point once it
is mounted, which is a change of its own.
@otavio
otavio force-pushed the fix/disk-space-check-measures-devtmpfs branch from a5810e0 to ea8b884 Compare July 28, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

NotEnoughSpace exception for update although we have anough space

1 participant