Measure real btrfs disk use without sudo
Contents
Run the host's own compsize as root in a throwaway Docker container to get the real on-disk size of a path on btrfs, with compression and reflinked copies counted correctly, when you are in the docker group but have no sudo.
- Tested on
- Fedora 44 workstation, kernel 7.2.10-200.fc44, btrfs
/homemounted withcompress=zstd:1, compsize 1.5^git20250123.d79eacf-15.fc44, coreutils 9.10, Docker CE 29.9.1 (rootful),alpineimage 3.24.2 (2026-10-25, on test files). Used on real data on the same workstation on 2026-10-23 or 2026-10-24 (tool versions not recorded). - Not verified
- a Docker daemon with SELinux support enabled (
selinuxindocker infosecurity options); rootless Docker or Podman themselves (only the user-namespace mechanism they rely on, Failure modes row 2); theAll empty or still-delalloced files.message compsize prints for data not yet written out. - Needs
- a btrfs filesystem;
compsizeinstalled on the host as/usr/bin/compsize; access to a rootful Docker daemon, usually through thedockergroup; thealpineimage, or network access to pull it. No sudo. - Time
- 5 min.
- Risk
- nothing on disk changes (except a root-owned
ro-testfile if the read-only check in Before you start fails), but this is not a low-privilege method. Thedockergroup grants root-level privileges (Docker's own docs). The container runs as root with CAP_SYS_ADMIN, and-v /:/host:roguards against your own mistakes, not against hostile code in the image. Run it only with an image you trust. The image provides onlychroot; the compsize binary and its libraries come from the host. Nothing to undo afterwards.
Before you start#
- Optional: a test directory with known content, to check the numbers before you trust them. Every transcript in this manual ran in one scratch directory on the same btrfs
/home; the measurements use these three 64 MiB files:$ head -c 64M /dev/urandom > random.bin $ cp random.bin random-copy.bin $ yes 'compsize demo: the quick brown fox jumps over the lazy dog' | head -c 64M > text.txt - compsize fails as your user, on any file on the btrfs filesystem (here
text.txtfrom the test directory). This is the error the manual works around:$ compsize text.txt text.txt: SEARCH_V2: Operation not permitted - The path is on btrfs, and compression is on:Expected:
$ findmnt -no FSTYPE,OPTIONS -T . btrfs rw,relatime,seclabel,compress=zstd:1,ssd,discard=async,space_cache=v2,subvolid=257,subvol=/homebtrfsand acompress=option. Use-T <path>for a path that is not the current directory. - compsize is installed on the host, at the path the container will call:Without it the
$ rpm -q compsize compsize-1.5^git20250123.d79eacf-15.fc44.x86_64 $ command -v compsize /usr/bin/compsizechroothas nothing to run, and installing it needs root. - The Docker daemon is rootful:Expected: no
$ docker info --format '{{.SecurityOptions}}' [name=seccomp,profile=builtin name=cgroupns]rootlessentry. A rootless daemon runs containers in a user namespace, where compsize fails the same way as for your user (Failure modes, row 2; rootless Docker itself not tested). - The read-only mount reaches the filesystem that holds your path.
/homecan be a separate mount under/; on this box it is (subvol=/homeabove). Docker makes the submounts of a read-only bind read-only too only as a best effort, on kernel 5.12 or newer:Expected:$ docker run --rm -v /:/host:ro -w "/host$PWD" alpine touch ro-test touch: ro-test: Read-only file systemRead-only file system. Iftouchsucceeds instead, the guard does not cover your path, and a root-ownedro-testfile is left behind. Remove it through a container, for exampledocker run --rm -v "$PWD":/w alpine rm /w/ro-test[UNVERIFIED: not exercised, the touch failed here]. - The
alpineimage is present, ordocker runpulls it on first use:Expected: an Alpine version. The measurement uses only its$ docker run --rm alpine cat /etc/alpine-release 3.24.2chroot. - Placeholders:
<path>is the file or directory to measure, as an absolute path ("$PWD/<name>"or~/…; a relative path printsNo files., Failure modes row 3).<name>is a file or directory in the current directory.<dir>is your test directory. - Backups: none needed. Nothing is written.
Steps#
- Flush pending writes.Expected: no output. compsize counts only data that already has extents on disk; its man page says recently written files can show as taking no space until they are synced or written out, typically within about 30 seconds.
sync - Run the host's compsize through the container. This is the command as used on real data:On the test directory:
docker run --rm --cap-add SYS_ADMIN -v /:/host:ro alpine chroot /host /usr/bin/compsize -x <path>Expected: a$ docker run --rm --cap-add SYS_ADMIN -v /:/host:ro alpine chroot /host /usr/bin/compsize -x "$PWD" Processed 3 files, 513 regular extents (514 refs), 0 inline, 325 fragments. Type Perc Disk Usage Uncompressed Referenced TOTAL 51% 66M 128M 192M none 100% 64M 64M 128M zstd 3% 2.0M 64M 64MProcessed N filesline, aTOTALrow, and one row per compression type.--cap-add SYS_ADMINgrants the capability that btrfs checks for the search ioctl compsize uses.-v /:/host:roshows the host's root read-only.chroot /hostmakes/usr/bin/compsizethe host's binary.-xstops at filesystem boundaries, and to compsize a nested btrfs subvolume is one (Failure modes, row 5). - Read the
TOTALrow againstdu.Expected:$ du -h random.bin random-copy.bin text.txt 64M random.bin 64M random-copy.bin 64M text.txt $ du -sh . 192M .dumatches Referenced (192M), not Disk Usage (66M). From the compsize man page and README: Disk Usage is what the files cost on disk, and every referenced extent is counted once; Uncompressed is the same extents before compression; Referenced is the apparent size of every file, so each reflinked copy counts again. Anonerow is data btrfs stored uncompressed. - Price duplicates as a set: all copies, then one.Expected: Disk Usage of all copies minus Disk Usage of one copy is what the extra copies really cost. Here it is 64M − 64M = 0, because
$ docker run --rm --cap-add SYS_ADMIN -v /:/host:ro alpine chroot /host /usr/bin/compsize -x "$PWD/random.bin" "$PWD/random-copy.bin" Processed 2 files, 1 regular extents (2 refs), 0 inline, 2 fragments. Type Perc Disk Usage Uncompressed Referenced TOTAL 100% 64M 64M 128M none 100% 64M 64M 128M $ docker run --rm --cap-add SYS_ADMIN -v /:/host:ro alpine chroot /host /usr/bin/compsize -x "$PWD/random.bin" Processed 1 file, 1 regular extents (1 refs), 0 inline, 1 fragments. Type Perc Disk Usage Uncompressed Referenced TOTAL 100% 64M 64M 64M none 100% 64M 64M 64Mcpmade a reflink: coreutils 9.0 and later default to--reflink=auto, and this box has 9.10. A duplicate finder that adds up file sizes would report 64M wasted.
Verify#
- A file with known content gives the expected cost. 64 MiB of repeated text takes 2.0M on disk:64 MiB of random data takes 64M, type
$ docker run --rm --cap-add SYS_ADMIN -v /:/host:ro alpine chroot /host /usr/bin/compsize -x "$PWD/text.txt" Processed 1 file, 512 regular extents (512 refs), 0 inline, 323 fragments. Type Perc Disk Usage Uncompressed Referenced TOTAL 3% 2.0M 64M 64M zstd 3% 2.0M 64M 64Mnone(step 4, second command).
Undo / rollback#
- Nothing on the filesystem changes: compsize only reads, and the host root is mounted read-only (checked in Before you start).
--rmremoves the container when it exits. Checked with a named run:$ docker run --rm --name compsize-manual-rmcheck-20261025 --cap-add SYS_ADMIN -v /:/host:ro alpine chroot /host /usr/bin/compsize -x "$PWD/text.txt" > /dev/null; docker ps -a --filter name=compsize-manual-rmcheck-20261025 --format '{{.Names}}' | wc -l 0- If
docker runpulledalpineonly for this,docker image rm alpineremoves it[UNVERIFIED: not exercised, the image was already present]. - Remove the test directory with
rm -r <dir>if you made one[UNVERIFIED: not exercised, the test files are kept as evidence].
Failure modes#
| Symptom (exact message) | Cause | Fix |
|---|---|---|
text.txt: SEARCH_V2: Operation not permitted (compsize run as your user; the file name comes first) | The btrfs TREE_SEARCH_V2 ioctl returns EPERM without CAP_SYS_ADMIN (kernel 7.2.10 fs/btrfs/ioctl.c). | Step 2. |
The same message from root in a user namespace: unshare -Ur shows CapEff: 000001ffffffffff and compsize still fails | The kernel checks the capability in the initial user namespace. Rootless Docker runs the daemon and its containers inside a user namespace (Docker docs); tested here with unshare -Ur only. | A rootful daemon, or an admin who runs compsize. |
No files. | A relative <path>. After chroot /host the working directory is / (checked with pwd), and compsize skips paths that do not exist without an error. | Pass an absolute path: "$PWD/<name>" or ~/…. A relative path given together with absolute ones is skipped the same way, so the total is partial with no error. |
du: cannot read directory './locked': Permission denied and a total that is too small | du runs as your user and skips directories it cannot read, such as a container's database directory with mode 700. | Use the container total: root in the container reads them. On the test directory, a 16 MiB file in a chmod 000 directory was missing from du -sh . (192M) and counted by compsize (Disk Usage 66M → 82M). |
No message, but files in a nested subvolume are missing: Processed 4 files instead of 5 | -x skips anything whose device number differs from its parent's, and every btrfs subvolume has its own. | Drop -x when the path holds subvolumes you want counted. On the test directory a 16 MiB file in a subvolume made 82M (with -x) into 98M (without). Without -x compsize also enters other mounts below the path, and per its source it stops at a non-btrfs one with Not btrfs (or SEARCH_V2 unsupported). (not exercised). |
Field notes#
- 2026-10-25, workstation: Fedora 44, kernel 7.2.10-200.fc44, btrfs compress=zstd:1 (
/home); compsize 1.5^git20250123.d79eacf-15.fc44; Docker CE 29.9.1 (rootful), alpine 3.24.2; coreutils 9.10, overlayfs storage, SELinux enforcing: Every block in this manual was run on test files in a scratch directory on the same/home, not on real data. As the normal user, compsize failed with the file name in front of the error:text.txt: SEARCH_V2: Operation not permitted. The container run worked first time.dusaid 192M for the directory, compsize said 66M on disk. The 64 MiBcpcopy cost nothing (one extent, two references). A compsize run started within a second of writing 64 MiB of text was expected to show the unflushed-data message, but all 512 extents were already counted, so thesyncin step 1 is a precaution from the man page, not an observed fix. Atouchthrough the read-only mount failed, so the read-only flag reached the/homesubmount on this kernel. Root in a user namespace (unshare -Ur, all capabilities) still got EPERM. A relative path printedNo files.A directory with mode 000 hid 16 MiB fromdubut not from the container, and-xsilently left out a nested test subvolume. The daemon has no SELinux support enabled, so this run says nothing about one that has. - 2026-10-23 or 2026-10-24 (the day was not recorded), same workstation and
/homefilesystem, tool versions not recorded: On the real disk, compsize ran without sudo, through the rootful Docker daemon (dockergroup), and each destructive step of the housekeeping waited for my approval.dureported uncompressed sizes. compsize run as my normal user failed on SEARCH_V2 withOperation not permitted, and the container command in step 2 worked. Result: 144 GB of data in 94 GB of disk. 78 GB of it was stored uncompressed: zips, ROMs, maps and model weights, data that btrfs's compression heuristic skips. A Postgres data directory owned by a container's uid (mode 700) madeduprintPermission deniedand undercount by 15.8 GB. A duplicate finder listed 268 sets of identical files of at least 5 MB, 12.4 GB of apparent waste. compsize per set (all copies, then one) put the real figure at 3.4 GB, becausecphad already made reflinks: a 2.9 GB speech model and its copy took 2.8 GB together. Recompressing withbtrfs filesystem defragment -czstd -L 9worked without root but gained about 5%, so it was dropped. A live SQLite database had them(no-compress) attribute on purpose.
Notes#
- Without Docker:
btrfs filesystem du -s <path>runs as a normal user and separatesExclusivefromSet shared, so it does see reflinked copies. It reports uncompressed sizes, though: on the test directory it gavetext.txt64.00MiB, against compsize's 2.0M. - Why
chrootinto the host instead of a compsize image: no package install inside a root container, and the binary is the host's own. Docker keeps theSYS_CHROOTcapability by default. ~/…paths work for<path>because the host shell expands them beforedockerruns (checked); relative paths do not (Failure modes, row 3).- Higher zstd level: about 5% on the original box, which was put down to btrfs compressing each 128 KiB chunk on its own (the chunk size is in the btrfs docs).
btrfs-filesystem(8)warns that defragmenting breaks up reflinks on kernels 3.14-rc2 and later, so do not defragment files whose copies share extents. - The story behind the numbers in the field notes, and why
dumisled: the btrfs duplicates that weren't.
Sources#
- compsize source,
compsize.c: the%s: SEARCH_V2: %merror,-xas a device-number comparison, missing paths skipped silently,No files.. Fedora's package is built from commitd79eacf, byte-identical to master when fetched. - compsize man page,
compsize.8: meaning of Disk Usage, Uncompressed and Referenced;-x; the caveat about unsynced files; "The ioctls used by this program require root." - compsize README: "every referenced extent is counted whole, exactly once"; the ioctl requires root.
- Linux 7.2.10
fs/btrfs/ioctl.c:btrfs_ioctl_tree_search_v2()returns-EPERMunlesscapable(CAP_SYS_ADMIN). - GNU coreutils NEWS, release 9.0: "cp now uses --reflink=auto mode by default."
- Docker: Linux post-installation steps: "The docker group grants root-level privileges to the user."
- Docker: security: the daemon needs root unless in rootless mode; only trusted users should control it.
- Docker: bind mounts: read-only binds make submounts read-only as a best effort, on kernel 5.12 or newer.
- Docker: running containers: default capabilities (
DAC_OVERRIDEandSYS_CHROOTkept,SYS_ADMINonly through--cap-add). - Docker: rootless mode: a rootless daemon lists
rootlessunder Security Options indocker info. - btrfs documentation: compression: data is split into 128 KiB chunks before compression; if the first portion compressed does not shrink, compression is disabled for the whole file.
btrfs-filesystem(8)from btrfs-progs 7.1 (man btrfs-filesystem): the defragment warning about breaking up reflinks.