Skip to main content

Ext4 Inodes and Extents: A Hands-On Exercise

First you will want to follow this guide to setup a logical disk with an ext4 filesystem.

Note: The filesystem is created with root as the owner, and without deep shell knowledge you may come up with commands that still wont work,

For Example:

sudo echo hello > abc.txt # bash: abc.txt: Permission denied

why sudo echo "hello" > file doesn’t work

  • sudo only applies to the echo command.

  • the > redirection is handled by your shell, before sudo even runs.

  • so what really happens:

    1. shell (as your normal user) opens file for writing.
    2. shell tries to redirect stdout of echo into it.
    3. permission denied → because you don’t have write perms.
    4. then echo runs under sudo, but too late — the file descriptor never opened.

Therefore I suggest making your user the owner:

sudo chown -R $USER:$USER mnt

1. Run this in your loop filesystem:

cd ~/fs-lab/mnt
echo "hello" > ./hello.txt
stat hello.txt
filefrag -v hello.txt
  • stat - shows size, block count, inode, timestamps.
  • filefrag -v - shows the physical extent mapping (1 extent found for our tiny file).

stat-and-filefrag

Looking deeper with debugfs

debugfs lets you peer into ext4 internals directly.

  • One-liner form (note the <inode> syntax):
sudo debugfs -R 'stat <12>' /dev/loop7
  • Or open a interactive shell (captive prompt - likely CTRL+Z to exit):
sudo debugfs /dev/loop7
debugfs: ls -l /
debugfs: stat <12>

debugfs

💡 Gotcha: if you try stat 12 without the angle brackets, debugfs interprets it as a filename. Use <12> to mean inode number.

2a. Watching extents grow

So far, hello.txt fits neatly into a single 4 KiB block → 1 extent. Let’s try to get multiple extents by making a larger file:

# write ~20 MB to force more blocks
dd if=/dev/zero of=bigfile.bin bs=1M count=20 status=progress
stat bigfile.bin
filefrag -v bigfile.bin | head -20

On a fresh filesystem you may still see just 1 extent (ext4 will happily allocate 20 MB contiguously if space is clean).

still-one-extent


2b. Forcing fragmentation

To really see multiple extents, try this:

# 1) Constrict contiguous free space by filling most of the FS
dd if=/dev/zero of=filler.bin bs=1M count=850 conv=fsync

# 2) Punch many scattered holes (Swiss-cheese the free space)
# Requires extents + fallocate hole punching (ext4 supports this).
for o in $(shuf -i 0-849 -n 80); do
fallocate --punch-hole --keep-size -o $((o*1024*1024)) -l $((512*1024)) filler.bin
done
sync

# 3) Now allocate a medium file into that fragmented space
dd if=/dev/zero of=story.bin bs=1M count=50 conv=fsync
sync

# 4) Inspect the extent layout (should be multiple)
filefrag -v story.bin | head -40

Now you should see several extents: contiguous runs where ext4 could grab space, split by gaps where it had to jump.

multiple-extents

Looking at that filefrag -v output:

  • Extent 0length: 4096 blocks = 4096 × 4 KiB = 16 MiB That’s your “big contiguous run” — ext4 found one nice chunk.

  • Extent 11919 blocks ≈ 7.5 MiB

  • Extent 2129 blocks ≈ 0.5 MiB

  • Extent 3 → another 1919 blocks ≈ 7.5 MiB

  • Extent 4 → another 129 blocks ≈ 0.5 MiB

  • Extent 51790 blocks ≈ 7.0 MiB

  • Extent 6 → just 2 blocks = 8 KiB (that’s the odd tail at EOF)

Add them up and you get ~39 MiB total, which matches my truncated story.bin (in my case it stopped early because the FS ran out of space). enospc ls-output

OK before we delete this file, lets grab the inode id and view debugfs:

ls -li story.bin

ls-li

sudo debugfs -R 'stat <23>' /dev/loop7

debugfs-output-story-dot-bin

3. Delete story.bin:

Lets delete the file and then inspect the inode!

rm ./story.bin
sudo debugfs -R 'stat <23>' /dev/loop7

after-rm

Anatomy of the “dead inode”

  • Inode number: 23 — same file we just deleted (story.bin).

  • Size: 0 / Blockcount: 0 / EXTENTS: (empty) → ext4 has cleared out all extent mappings and released the blocks back to the free-space bitmap.

  • Links: 0 → no directory entries point to this inode anymore (so it’s officially unreachable from the FS).

  • dtime (deletion time): Tue Aug 19 15:31:05 2025 → the critical field set when a file is unlinked and its inode marked free.

  • ctime / mtime / atime:

    • mtime (modified) and atime (accessed) still show the last use of the file before deletion.
    • ctime reflects when inode metadata last changed (here, the moment of deletion).
    • The new dtime is essentially the tombstone.
  • Flags 0x80000 → this was just a regular file.

  • Checksum: ext4 keeps a checksum for inode integrity, still valid even after deletion.