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 theecho
command. -
the
>
redirection is handled by your shell, beforesudo
even runs. -
so what really happens:
- shell (as your normal user) opens
file
for writing. - shell tries to redirect stdout of
echo
into it. - permission denied → because you don’t have write perms.
- then
echo
runs under sudo, but too late — the file descriptor never opened.
- shell (as your normal user) opens
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).
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>
💡 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).
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.
Looking at that filefrag -v
output:
-
Extent 0 →
length: 4096
blocks = 4096 × 4 KiB = 16 MiB That’s your “big contiguous run” — ext4 found one nice chunk. -
Extent 1 →
1919
blocks ≈ 7.5 MiB -
Extent 2 →
129
blocks ≈ 0.5 MiB -
Extent 3 → another
1919
blocks ≈ 7.5 MiB -
Extent 4 → another
129
blocks ≈ 0.5 MiB -
Extent 5 →
1790
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).
OK before we delete this file, lets grab the inode id and view debugfs:
ls -li story.bin
sudo debugfs -R 'stat <23>' /dev/loop7
3. Delete story.bin:
Lets delete the file and then inspect the inode!
rm ./story.bin
sudo debugfs -R 'stat <23>' /dev/loop7
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) andatime
(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.