Filesystems & mounts

ext4, xfs, mount options, and repair.

Advanced12 min · lesson 9 of 17

A raw disk is a warehouse full of identical, numbered boxes. Nothing in it knows what a file is, which boxes belong together, or which ones are empty. A filesystem is the warehouse's inventory clerk. It keeps the ledger that says the file /etc/shadow lives in these blocks, was last written on Tuesday, and only root may read it. On Linux, the filesystem is the layer that turns a partition or logical volume (a slice of raw disk the system treats as one unit) into named files and directories you can actually open.

Ext4 And Xfs, And Why The Choice Rarely Bites

ext4 (the fourth extended filesystem, the long-standing general-purpose default on Debian and Ubuntu) is reliable and boringly well understood. If you do nothing special, this is what you get, and for most workloads that is the right answer. xfs (a high-performance filesystem that Silicon Graphics wrote for their workstations, now the default on Red Hat Enterprise Linux and its rebuilds) is built for large files and for many processes reading and writing at the same time. For a busy database or a box shuffling huge media files, xfs pulls ahead. For most servers either is fine, and the difference only shows at the extremes of size and concurrency.

Both are journaling filesystems. A journal here works like a chef who writes "about to move the roast from oven to counter" on a notepad before doing it. If the power dies mid-step, you read the last note and either finish the action or undo it cleanly, instead of finding a half-updated mess. The filesystem writes its intended change to a small log first, then performs it. After a crash, the kernel (the core of the operating system that talks directly to the hardware) replays or discards that log on the next mount, so you get a consistent filesystem back rather than a corrupt one. This is why you almost never run a manual repair on a modern box.

One practical difference is worth knowing before you commit. ext4 can grow and shrink. xfs can only grow, never shrink. If you might ever need to hand space back from a filesystem, that constraint alone can decide it for you.

Seeing What You Have

Before you touch anything, look. lsblk -f draws the block-device tree and hangs each filesystem's type, label, UUID (universally unique identifier, a stable random id written into the filesystem when it is created) and mount point off it. df (disk free) tells you how full each mounted filesystem is; the -T flag adds a Type column and -h makes the sizes human-readable. du answers the other question, which directories are actually eating the space. Between them you see the layout, the free space, and the hog, without guessing.

~/secopslog — bash
$ lsblk -f # block devices, filesystem types, and where they're mounted
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS sda ├─sda1 vfat FAT32 BOOT A1B2-C3D4 505.4M 1% /boot/efi └─sda2 ext4 1.0 root 8f3a2b1c-4d5e-6f70-8a9b-0c1d2e3f4a5b 17.9G 52% / sdb LVM2_member └─data--vg-app xfs app b1c2d3e4-5f60-7182-93a4-b5c6d7e8f901 92.1G 8% /srv/app
$ df -Th # -T adds the filesystem TYPE column, -h makes sizes readable
Filesystem Type Size Used Avail Use% Mounted on /dev/sda2 ext4 40G 20G 18G 53% / tmpfs tmpfs 3.9G 0 3.9G 0% /dev/shm /dev/mapper/data--vg-app xfs 100G 8.5G 92G 9% /srv/app

Mounting, And The File That Survives Reboots

A fresh filesystem is a room with no door. Mounting is hanging the door: you attach the filesystem to a point in the directory tree, and from then on everything under that point lives on that disk. The mount command does it right now, for this boot, and forgets it at reboot. To make a mount stick, you write it into /etc/fstab (the filesystem table), the list the system reads at every boot to decide what to attach where. First you need a filesystem to mount, which mkfs (make filesystem) creates, and its UUID, which blkid prints.

~/secopslog — bash
$ sudo mkfs.ext4 -L logs /dev/data-vg/logs # make an ext4 filesystem, label it 'logs'
mke2fs 1.46.5 (30-Dec-2021) Creating filesystem with 262144 4k blocks and 65536 inodes Filesystem UUID: 3f2a9c8b-1d4e-4a6f-9b2c-7e8f0a1b2c3d Superblock backups stored on blocks: 32768, 98304, 163840, 229376 Allocating group tables: done Writing inode tables: done Creating journal (8192 blocks): done Writing superblocks and filesystem accounting information: done
$ blkid /dev/data-vg/app # print a device's UUID and type
/dev/data-vg/app: LABEL="app" UUID="b1c2d3e4-5f60-7182-93a4-b5c6d7e8f901" TYPE="xfs"

Each line in fstab has six fields: the device, where to mount it, the filesystem type, a comma-separated list of options, and two small numbers. Identify the device by UUID, not by /dev/sdb1. Kernel device names are assigned in the order disks are found, so add a drive and yesterday's sdb can come up as sdc today, while the UUID never moves. The fifth field (dump) is a leftover from an old backup tool and is 0 on every modern system. The sixth field (pass) sets the boot-time check order: 1 for the root filesystem, 2 for the rest, 0 to skip. xfs always uses 0 there, because it never checks at boot; it recovers its journal on mount instead.

/etc/fstab
# <device> <mountpoint> <type> <options> <dump> <pass>
UUID=8f3a2b1c-4d5e-6f70-8a9b-0c1d2e3f4a5b / ext4 defaults,noatime 0 1
UUID=b1c2d3e4-5f60-7182-93a4-b5c6d7e8f901 /srv/app xfs defaults,noatime 0 0
UUID=c3d4e5f6-6071-8293-a4b5-c6d7e8f90123 /var/log ext4 defaults,nofail,nodev,nosuid 0 2
UUID=d4e5f6a7-7182-93a4-b5c6-d7e8f9012345 /tmp ext4 defaults,nodev,nosuid,noexec 0 2
# nofail: don't block boot if the disk is missing. Find a UUID with: blkid /dev/data-vg/app

After you edit fstab, do not reboot on faith. mount -a tries to mount everything in the file that is not already mounted, against the running system. If a line is wrong, you find out now, at a shell, instead of at the next boot with no shell. findmnt --verify goes further and parses the whole file for mistakes without mounting anything.

~/secopslog — bash
$ sudo mount -a # apply fstab against the running system; no output means success echo $?
0
A bad fstab line can leave a host unbootable
systemd waits for each fstab mount before it lets boot finish, so a typo or a disk that is not present can hang the machine or drop it to an emergency shell with no network. Three habits keep you safe: mark any non-essential mount nofail so a missing disk cannot stop boot, identify devices by UUID, and run findmnt --verify before rebooting. Watch one trap: mount -a skips entries that are already mounted, so if you change the options on a filesystem that is currently mounted, mount -a reports success while doing nothing. To actually apply new options to a live mount, use mount -o remount,<options> <target>.

Mount Options Are A Security Control

Options are the house rules for a mounted filesystem, and some of them are security controls, not tuning knobs. Start with the tame one. noatime tells the filesystem to stop recording a "last read" timestamp every time a file is opened. By default Linux uses relatime, which already writes that timestamp rarely; noatime turns it off entirely, which on a busy filesystem is a real cut in write traffic. The cost lands on defenders: access times are one signal in an intrusion timeline, and noatime erases it, so weigh that on hosts you may have to investigate later.

The three that matter for hardening are nosuid, nodev, and noexec. Here is the attack they blunt. The setuid bit on a program tells the kernel to run it as the file's owner, so a program owned by root runs as root no matter who launches it. That is how sudo works, and it is also the backbone of a classic backdoor. An attacker who gets root even once can leave a root-owned copy of a shell with the setuid bit set sitting in a writable directory. From then on any user who runs it lands in a root shell, no password asked. Mount that directory nosuid and the kernel ignores the setuid bit there, so the backdoor is dead weight. nodev tells the kernel to ignore device nodes on the mount, which stops an attacker planting a fake /dev/sda in a writable directory to read the raw disk straight past file permissions. noexec refuses to run any binary from the mount at all. On the directories every user can write to (/tmp, /var/tmp, /dev/shm), those three together take away the easiest place to stage and launch a payload.

Check what is actually applied with findmnt, which shows the real options the kernel is using, not what you hoped you set. This is also how you find gaps. On a stock Ubuntu box, /dev/shm (a RAM-backed shared-memory filesystem) comes mounted nosuid,nodev but without noexec, which is exactly why some malware runs straight out of it. findmnt also reveals bind mounts and overlay mounts, the kind an attacker uses to shadow a legitimate path with their own file, so read it carefully on any host you suspect.

~/secopslog — bash
$ findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS /tmp /dev/shm
TARGET SOURCE FSTYPE OPTIONS /tmp /dev/mapper/data--vg-tmp ext4 rw,nosuid,nodev,noexec,relatime /dev/shm tmpfs tmpfs rw,nosuid,nodev

Keep noexec in perspective. It stops you running a binary from the mount directly, but an interpreter is itself a program the kernel is glad to load, and it will happily run a script you hand it. python3 /tmp/x.py, bash /tmp/x.sh, or pulling code in as a shared library all walk around the flag. Treat nodev, nosuid, and noexec on /tmp and its siblings as one layer that clears out the laziest attacks, not a guarantee that nothing runs from there.

Mount options, grouped by what they buy you
Performance
noatime
stop writing a timestamp every time a file is read
nodiratime
same, for directories (already implied by noatime)
Hardening
nosuid
ignore the setuid bit, so a dropped root shell stays unprivileged
nodev
ignore device nodes, so a fake /dev/sda cannot bypass access control
noexec
refuse to execute binaries from this mount
ro
read-only: nothing can be written or changed
Boot safety
nofail
a missing disk will not hang the boot
UUID=
identify the disk by stable id, not a shifting /dev name
Options are comma-separated in the fstab line; combine them freely, e.g. defaults,nodev,nosuid,noexec.

Checking And Repairing

When a filesystem does get damaged, from a bad shutdown or a dying disk, the check-and-repair tools can only work safely on an unmounted filesystem. Repairing a mounted filesystem underneath the running kernel is like rebuilding an engine while the car is driving. So you unmount first, then check. For ext4 the tool is fsck (filesystem check), usually run as fsck -fy: -f forces a full check even if the superblock (the filesystem's header block that records its overall state) claims the filesystem is clean, and -y answers yes to every repair prompt so it runs unattended.

~/secopslog — bash
$ sudo umount /dev/data-vg/logs # fsck needs it UNMOUNTED sudo fsck -fy /dev/data-vg/logs # -f force full check, -y auto-answer yes
fsck from util-linux 2.37.2 e2fsck 1.46.5 (30-Dec-2021) Pass 1: Checking inodes, blocks, and sizes Pass 2: Checking directory structure Pass 3: Checking directory connectivity Pass 4: Checking reference counts Pass 5: Checking group summary information /dev/mapper/data--vg-logs: 328/65536 files (0.6% non-contiguous), 12894/262144 blocks

xfs does not work this way, and this catches people. Running fsck on an xfs filesystem does nothing useful: fsck.xfs is a stub that exits successfully without looking at anything, because xfs checks and repairs with a separate tool, xfs_repair. If someone unmounts an xfs volume, runs fsck, sees a clean exit and calls it fixed, they have inspected nothing. The real command is xfs_repair, also on an unmounted filesystem.

~/secopslog — bash
$ sudo umount /srv/app sudo xfs_repair /dev/data-vg/app # fsck.xfs does NOTHING; this is the real tool
Phase 1 - find and verify superblock... Phase 2 - using internal log - zero log... - scan filesystem freespace and inode maps... - found root inode chunk Phase 3 - for each AG... - scan and clear agi unlinked lists... - process known inodes and perform inode discovery... - process newly discovered inodes... Phase 4 - check for duplicate blocks... - setting up duplicate extent list... - check for inodes claiming duplicate blocks... Phase 5 - rebuild AG headers and trees... - reset superblock... Phase 6 - check inode connectivity... - traversing filesystem ... - traversal finished ... - moving disconnected inodes to lost+found ... Phase 7 - verify and correct link counts... done

The root filesystem is the awkward case: you cannot unmount it while the system is running on it. You have two moves. To inspect it without unmounting, read the superblock: tune2fs -l on ext4, or xfs_info on xfs, both safe on a live mount, and tune2fs will report the filesystem state. To actually repair root, force a check on the next boot, before root is in full use. Add fsck.mode=force (and fsck.repair=yes) to the kernel command line from the boot menu (GRUB, the bootloader that loads the kernel), or on many systems create the old flag file and reboot into the check.

~/secopslog — bash
$ sudo tune2fs -l /dev/sda2 | head # read the superblock; safe while mounted
tune2fs 1.46.5 (30-Dec-2021) Filesystem volume name: root Last mounted on: / Filesystem UUID: 8f3a2b1c-4d5e-6f70-8a9b-0c1d2e3f4a5b Filesystem magic number: 0xEF53 Filesystem revision #: 1 (dynamic) Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum Filesystem flags: signed_directory_hash Default mount options: user_xattr acl Filesystem state: clean
$ # Root FS can't be unmounted live. Force a check on the NEXT boot: # at the GRUB menu press 'e', add to the linux line: fsck.mode=force fsck.repair=yes # The legacy flag file still works on many systems: sudo touch /forcefsck && sudo systemctl reboot

One habit ties this together: whenever you change storage, verify the result out of band. After mkfs, run lsblk -f and confirm the type and UUID. After editing fstab, run findmnt --verify, apply live changes with mount -o remount, and read back the real options with findmnt before you ever reboot. The reboot is the moment a quiet mistake turns into a downed host, and every one of these checks happens before it.

Quick check
01A colleague suspects an xfs volume is damaged. They unmount it, run fsck -y against it, watch it exit 0, and report the filesystem repaired. What actually happened?
Incorrect — fsck drives the ext4 checker, and that is where the block-level pass lives. On xfs the command never reaches any checking logic, so the exit code is not evidence of anything.
Correct — xfs keeps its checking and repair logic in its own tool. Until you type xfs_repair yourself, against an unmounted volume, no metadata has been examined.
Incorrect — There is no such handoff. If you want xfs_repair to run, you invoke it directly, because nothing on the fsck side will call it for you.
Incorrect — Journal replay on mount is real, and it is why manual repairs are rare, but it is not what produced this exit code. Note also that -f exists precisely so you can force a check past a clean superblock on ext4.
02A writable directory holds a root owned copy of a shell with the setuid bit set. You remount that filesystem nosuid. What changes for the next user who runs that file?
Incorrect — That is what noexec buys you. nosuid leaves execution untouched and only changes whose identity the running program is given.
Incorrect — You are describing nodev. It defeats a fake device node placed in a writable directory, which is a separate trick from a planted setuid binary.
Incorrect — Mount options generate no audit records, and anything that still hands out root leaves the backdoor fully working rather than blunted.
Correct — With the owner's identity no longer granted on that mount, the planted binary drops to being just another program the caller already had the right to run.
03You add noexec to the /var/log line in /etc/fstab, run sudo mount -a, and it exits 0. findmnt still shows /var/log mounted with only nodev and nosuid. What happened, and what do you run next?
Correct — Your fstab is now correct for the next boot, and the remount is what closes the gap between the file on disk and the options the running kernel is enforcing.
Incorrect — The pass field only orders boot-time checks, and 2 is the normal value for a non-root filesystem. It has no bearing on when mount options take effect.
Incorrect — noexec is enforced by the kernel for any filesystem type, ext4 included, and it already sits on the /tmp line in this same file. The option was never the problem.
Incorrect — It rebuilds nothing. Entries that are already mounted are left exactly as the kernel has them, whatever you changed in the file, which is why the run looked successful.

Try this

Work through “Checking And Repairing” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: a bad fstab line can leave a host unbootable. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related