Signal9Signal9 — home
online
all lessons

ext4 & XFS Internals for Responders

Intermediate35 min7 steps
What you'll learn
  • Explain why on-disk filesystem structure governs what you can recover, how accurate your timeline is, and whether you can spot tampering
  • Separate the inode (the real file object) from the directory entry (just a name pointing at it) and reason about what survives a 'delete'
  • Read an ext4 superblock and block-group layout with dumpe2fs/tune2fs and walk an inode with debugfs
  • Recover the hidden crtime/btime birth timestamp that plain ls and stat don't show, and stop conflating it with ctime
  • Adapt your tooling and mental model to XFS — xfs_info/xfs_db, B+tree allocation, and journal semantics that differ from ext4
  1. Why a responder cares about on-disk structure

    Frame filesystem internals as the foundation under recovery, timeline accuracy, and tamper detection.

    In File System Forensics you read timestamps and hunted hidden names with stat, ls, and find. Those tools hand you a tidy, filtered view of the disk. This lesson goes one layer down — to the structures the kernel actually writes to the platter — because three core jobs of incident response are impossible to do well without that layer:

    • Recovery. When a file is 'deleted,' how much of it is really gone? The answer depends entirely on what the filesystem stores and where. You can't make a recovery call without knowing the on-disk model.
    • Timeline accuracy. A timeline is only as trustworthy as the timestamps feeding it. Where those timestamps physically live — and which ones the standard tools quietly hide from you — decides whether your timeline is complete or has a blind spot.
    • Tamper detection. Spotting a forged file means understanding what must be consistent on disk and what an attacker can or can't rewrite. You can't spot an inconsistency you don't know should exist.

    The filesystem isn't a black box that 'stores files.' It's a records system, and a responder who understands the records reads them the way an auditor reads a ledger.

  2. The inode vs. the directory entry

    Separate the real file object from its name, and see why a deleted name can leave the file intact.

    Here is the single most important idea in this lesson: a file is not its name. On Linux, the real file — owner, permissions, size, timestamps, and the pointers to its data blocks — lives in a fixed-size record called an inode. The name you see in a directory is a separate, tiny structure called a directory entry (dentry), and all it contains is a name and the inode number it points to.

    Think of the inode as a shipping manifest in a warehouse: it lists exactly what's in the crate, who owns it, when it arrived, and which shelf locations (data blocks) hold the contents. The directory entry is just a luggage tag tied to the crate's handle, printed with a human-friendly label and the manifest's ID number. Tear the luggage tag off and throw it away, and the crate, its contents, and its manifest are all still sitting on the shelf — you've only removed the label that pointed to them.

    This is exactly what 'deleting' a file usually does. rm removes the directory entry (the tag) and decrements the inode's link count. If nothing else links to that inode, the filesystem marks the inode and its data blocks as free — but it does not scrub them. Until something reuses that space, the manifest and the crate's contents are recoverable. That's the whole basis of deleted-file recovery.

    ls -i /etc/hostname /etc/hosts
    262145 /etc/hostname  262148 /etc/hosts
    -i
    Print each file's inode number — the real ID of the underlying object, not its name.
    The numbers 262145 and 262148 are inode numbers — the addresses of the actual file objects. The path /etc/hostname is just a label in the /etc directory entry that points at inode 262145. Two completely different names can point at the same inode (that's a hard link), and renaming a file changes the directory entry while leaving the inode — and inode number — untouched. The name is mutable bookkeeping; the inode is the file.

    Check yourself

    You run rm secret.txt and the file vanishes from ls. In filesystem terms, what was actually removed, and what is likely still on disk?

  3. The map of the disk: superblock & block groups

    Read the ext4 superblock with dumpe2fs and understand the block-group layout.

    If inodes are the manifests, the superblock is the front-desk master ledger for the whole warehouse: how big the filesystem is, the block size, how many inodes exist and how many are free, the volume UUID, the mount count, and which optional features are enabled. There's a primary superblock near the start of the device and backup copies scattered across the disk — because if the master ledger is corrupted, recovery depends on those backups.

    ext4 doesn't store all its inodes in one pile. It divides the disk into block groups, each a self-contained chunk with its own slice of inodes, its own data blocks, and bitmaps tracking what's used. Spreading metadata across groups keeps related data physically close (less seeking) and means damage to one region doesn't doom the entire filesystem. For a responder, the practical payoff is knowing where to look: the superblock tells you the inode count and block size you need to interpret everything else, and the group layout explains how inodes map to physical locations.

    First, always confirm which filesystem you're standing on — the tooling forks completely from here.

    findmnt -no FSTYPE,SOURCE /
    ext4 /dev/vda1
    -n
    No header line — just the values, easy to read or capture into a variable.
    -o FSTYPE,SOURCE
    Print only the filesystem type and the backing device, in that order.
    /
    Ask specifically about the mount at the root filesystem.
    Two facts that decide everything downstream: the root filesystem is ext4, living on the device /dev/vda1. If this said xfs, you'd skip dumpe2fs/debugfs entirely and reach for xfs_info/xfs_db instead. Capturing the device path here is also handy — the next commands operate on the raw device, not the mount point.

    Example — not runnable on your lab box

    sudo dumpe2fs -h /dev/vda1
    dumpe2fs 1.46.5 (30-Dec-2021)
    Filesystem volume name:   <none>
    Filesystem UUID:          7c9f1a2b-3d4e-5f60-8a90-b1c2d3e4f5a6
    Filesystem features:      has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file
    Default mount options:    user_xattr acl
    Inode count:              655360
    Block count:              2621440
    Free blocks:              1980233
    Free inodes:              612904
    First block:              0
    Block size:               4096
    Inodes per group:         8192
    Inode size:               256
    Journal inode:            8
    Journal backup:           inode blocks
    -h
    Header only — print the superblock summary and skip the long per-group dump (which can be thousands of lines).
    This is the master ledger in plain text. Block size 4096 and Inode size 256 are the units you'll use to interpret every other structure (and the 256-byte inode is what makes room for the extra timestamp you'll meet next). Inodes per group: 8192 confirms the block-group design. has_journal and Journal inode: 8 tell you ext4 keeps a journal — central to recovery, covered shortly. The UUID uniquely identifies this filesystem for evidence labeling. tune2fs -l /dev/vda1 prints the same superblock fields; tune2fs can also change some of them (mount counts, labels), so on evidence you only ever read with -l.

    Check yourself

    Which single structure holds the block size, inode count, UUID, and enabled feature flags for the whole ext4 filesystem — and has backup copies for resilience?

  4. Walking an inode: debugfs & the hidden birth time

    Open the inode directly with debugfs and recover the crtime/btime that ls and stat hide.

    stat shows you three timestamps — atime, mtime, ctime — and stops. But a 256-byte ext4 inode actually stores a fourth: crtime, the creation (birth) time, also called btime. This is the genuine answer to 'when did this file first come into existence' — and the classic stat() API never exposed it, which is why generations of responders learned to (wrongly) treat ctime as 'created.' It isn't: ctime is inode-change time. The real birth time was there on disk all along; the tools just didn't surface it.

    debugfs is the ext4 internals console. It lets you open the raw filesystem and inspect inodes directly — by number or by path — bypassing the friendly abstractions. It's the responder's window into the manifest exactly as it sits on the platter.

    sudo debugfs -R 'stat <2>' /dev/vda1
    debugfs 1.46.5 (30-Dec-2021)
    Inode: 2   Type: directory    Mode:  0755   Flags: 0x80000
    Generation: 0    Version: 0x00000000:00000001
    User:     0   Group:     0   Size: 4096
    File ACL: 0
    Links: 24   Blockcount: 8
    Fragment:  Address: 0    Number: 0    Size: 0
     ctime: 0x66780a40:00000000 -- Sun Jun 23 02:10:08 2026
     atime: 0x6878a1f0:00000000 -- Mon Jun 23 09:01:04 2026
     mtime: 0x66780a40:00000000 -- Sun Jun 23 02:10:08 2026
    crtime: 0x65a1b3c0:1a2b3c4d -- Fri Jan 12 04:55:28 2024
    Size of extra inode fields: 32
    EXTENTS:
    (0):9412
    -R 'stat <2>'
    Run one command non-interactively. stat dumps an inode; <2> means inode number 2 — by convention always the root directory /.
    <N>
    Angle brackets address a raw inode by number; without brackets debugfs treats the argument as a path.
    Inode 2 is the root directory / on every ext4 filesystem — a reliable, always-present target. The breakthrough line is crtime: the file's true birth time (Jan 2024 here), a field plain stat never prints. Notice ctime and mtime match (Jun 23) but crtime is far older — exactly the kind of detail that distinguishes a long-lived directory from a freshly planted file. EXTENTS shows ext4's modern allocation: instead of listing every block, it records contiguous ranges, which is faster and resists fragmentation. To inspect a named file instead, drop the brackets: stat /etc/passwd.

    Check yourself

    ext4 stores a creation timestamp on disk, but stat traditionally doesn't print it and people substitute ctime for it. What is that real on-disk birth timestamp called?

  5. The journal: why it matters for recovery & RO mounting

    Understand ext4 journaling modes and why they shape safe evidence handling.

    A filesystem write is rarely a single atomic act — updating one file can mean touching the inode, the data blocks, and several bitmaps. If power dies halfway through, you'd be left with a half-finished change and an inconsistent disk. ext4's journal prevents that. It's a reserved area (you saw Journal inode: 8 in the superblock) where the filesystem first records what it's about to do. After a crash, it replays the journal to finish or roll back incomplete operations, so the disk comes back consistent without a full scan.

    The analogy: the journal is a construction site's work-order log. Before any crew touches the building, the change is written into the log. If a storm hits mid-job, the foreman reads the log and either completes the half-done work order or tears it back out — the building never ends up in a broken in-between state.

    Ext4 offers three journaling modes that trade safety for speed:

    • journal — both metadata and file data go through the journal. Safest, slowest.
    • ordered (the default) — only metadata is journaled, but data blocks are forced to disk before their metadata commits. A strong safety/speed balance.
    • writeback — only metadata is journaled, with no ordering guarantee for data. Fastest, but after a crash a file's metadata can point at stale or garbage blocks.
    sudo dumpe2fs -h /dev/vda1 2>/dev/null | grep -iE 'journal|mount options'
    Default mount options:    user_xattr acl
    Journal inode:            8
    Journal backup:           inode blocks
    Journal features:         journal_64bit
    Journal size:             64M
    grep -iE 'journal|mount options'
    Case-insensitive extended grep to pull just the journal and mount-option lines out of the superblock dump.
    2>/dev/null
    Discard the version banner that dumpe2fs prints to stderr, keeping the output clean.
    The journal is present (inode 8, 64M). The active mode isn't always spelled out here — data=ordered is the default — but you can confirm a live mount's mode with cat /proc/mounts | grep ' / ' and look for data=ordered|writeback|journal. For a responder the takeaway is procedural: the journal contains recent, possibly un-replayed changes. Mounting a piece of evidence read-write would let the kernel replay the journal and alter the disk, destroying the very state you're trying to preserve.

    Check yourself

    Why is mounting a piece of ext4 evidence read-WRITE dangerous, specifically because of the journal?

  6. XFS for responders: different structures, different tools

    Map your ext4 knowledge onto XFS and switch to its tooling and recovery semantics.

    Walk onto a RHEL/Rocky 8 box and findmnt will likely say xfs, not ext4. XFS stores the same concepts — inodes, directories, a superblock, journaling, timestamps including a real birth time — but the structures and, critically, the tooling are different. None of your dumpe2fs/debugfs/tune2fs muscle memory works here; reach for those on XFS and you'll just get errors.

    The big structural differences a responder should internalize:

    • Allocation is B+tree-based. Where ext4 uses block-group bitmaps and extents, XFS organizes free space and inodes with B+trees within allocation groups (its rough analog of block groups). The effect is excellent scaling to huge filesystems and many parallel operations — but it means recovery and analysis tools must understand the trees, not bitmaps.
    • Inodes are allocated dynamically, not pre-created at format time, so 'inodes per group' isn't a fixed up-front number the way ext4's superblock reports it.
    • The journal is internal and replay-on-mount, like ext4's, with the same evidence caution — but it has no ext-style external-journal-device replay tuning and no data= ordered/writeback knob; XFS journals metadata only, by design.
    • XFS is grow-only. You can xfs_growfs a mounted filesystem larger, but XFS cannot be shrunk — there's no supported shrink. That's an operational fact worth knowing, and a hint that XFS is built for large, growing storage.

    The tooling maps across cleanly once you know the pairs: dumpe2fs → `xfs_info`/`xfs_db`, debugfs → `xfs_db`, fsck.ext4 → `xfs_repair`.

    Example — not runnable on your lab box

    sudo xfs_info /
    meta-data=/dev/vdb1              isize=512    agcount=4, agsize=655360 blks
             =                       sectsz=512   attr=2, projid32bit=1
             =                       crc=1        finobt=1, sparse=1, rmapbt=0
    data     =                       bsize=4096   blocks=2621440, imaxpct=25
             =                       sunit=0      swidth=0 blks
    naming   =version 2              bsize=4096   ascii-ci=0, ftype=1
    log      =internal log           bsize=4096   blocks=2560, version=2
    realtime =none                   extsz=4096   blocks=0, rtextents=0
    xfs_info /
    Print the XFS geometry for the filesystem mounted at / — the XFS equivalent of dumpe2fs -h.
    The XFS counterpart to the ext4 superblock dump. agcount=4 is the number of allocation groups (XFS's parallel to ext4 block groups). isize=512 is the inode size — bigger than ext4's 256, leaving room for the birth timestamp and extended attributes. crc=1 means metadata is checksummed, so corruption (or tampering) in metadata is detectable. log=internal log is the journal living inside the filesystem. For deep inode work you'd drop to xfs_db -r /dev/vdb1 (the -r is read-only — the forensically safe way to open it) and use commands like inode <N> and print.

    Check yourself

    You confirm a server's root filesystem is XFS. Which command gives you the geometry (allocation groups, inode size, journal) — the rough equivalent of dumpe2fs -h on ext4?

  7. Try it in your live terminal

    Read your own filesystem's internals end-to-end on the live sandbox — all read-only.

    You have a real root shell. Run this sequence in order — it's the exact 'identify the filesystem, then read its internals' opening you'd use on a disk-forensics call. Every command here is read-only; nothing modifies the filesystem. The point is to make the moves reflex: first find out what you're on, then read the inode and its hidden timestamps, then drop into the right internals tool for your filesystem.

    findmnt -no FSTYPE,SOURCE /
    Step zero: what is / actually on? On a bare-metal or VM host you get ext4 /dev/vda1 or xfs /dev/sda2 and you're done. On this sandbox — and on any container — you get `overlay none`: there is no block device behind / at all, so every internals tool below would fail on it. That's the next command's job.
    findmnt -t ext4,xfs -nlo SOURCE,TARGET,FSTYPE
    When / is an overlay, network mount, or tmpfs, ask instead which mounts do sit on a real filesystem. Here that finds /dev/vdb[/upper] /.fly-upper-layer ext4 — the writable ext4 layer under the container. lsblk -f is the wider view when nothing is mounted yet. The bracketed [/upper] is the subvolume path within the device; strip it before handing the device to a tool: DEV=$(findmnt -t ext4,xfs -nlo SOURCE | head -1 | sed 's/\[[^]]*\]//'); echo "$DEV".
    stat .
    Read the current directory's inode number and its three classic timestamps (Access/Modify/Change). Remember: the Change line is ctime, NOT creation. Try stat --printf '%w\n' . too — if your tooling supports it, that prints the real birth time (crtime); older versions show -.
    ls -i
    List the inode number beside each name in this directory. Each number is the real file object; the name is just a directory entry pointing at it. Renaming a file would change the name but keep this number.
    sudo dumpe2fs -h "$DEV" 2>&1 | head -20
    Read the on-disk map of the device you just resolved into $DEV. On ext4 this prints the superblock — note Block size, Inode size, and Journal inode: 8. On xfs, dumpe2fs says so plainly and you run sudo xfs_info "$DEV" instead. Keep 2>&1 rather than 2>/dev/null: pointing an ext4 tool at a device that isn't ext4 gives you Couldn't find valid filesystem superblock, which is an answer — silencing it leaves you staring at nothing.
    sudo debugfs -R 'stat <2>' "$DEV" 2>&1 | head -14
    On ext4 this dumps root's inode (number 2, always / on that filesystem) — watch for the crtime line that plain stat hides, and the EXTENTS list of data ranges. On the sandbox's ext4 layer it prints Inode: 2 Type: directory Mode: 0755, then ctime/atime/mtime/crtime in both hex and human form. On XFS the read-only equivalent is sudo xfs_db -r -c 'sb 0' -c print "$DEV". All of these are read-only; none touches the disk.
Recap
  • A file is its inode, not its name. The directory entry is just a name→inode tag — delete the tag and the inode and data blocks usually survive, which is the entire basis of deleted-file recovery.
  • The superblock is the master ledger (block size, inode count, UUID, features, journal) with backup copies; ext4 spreads inodes across block groups for locality and resilience.
  • ctime is not creation time. ext4's real birth timestamp is crtime/btime, stored in the inode and hidden from plain ls/stat — read it with debugfs stat, stat --printf '%w', or statx to avoid a timeline blind spot.
  • ext4's journal makes the disk crash-safe by replaying pending changes — so a careless read-write mount replays and alters evidence. Mount evidence ro,noload or use a write blocker.
  • XFS stores the same concepts with B+tree allocation groups and grow-only sizing, but needs different tooling: xfs_info/xfs_db/xfs_repair, not dumpe2fs/debugfs/fsck.ext4. Run findmnt -no FSTYPE first, every time.
ext4 structure → XFS equivalent → what it tells a responder
ext4XFS equivalentWhat it tells a responder
Inode (256B)Inode (512B, dynamic)The real file: owner, mode, size, timestamps incl. crtime, block/extent pointers. Survives a deleted name.
Directory entryDirectory entry (B+tree)Just a name→inode link. Removing it is what rm does — the inode/data often remain recoverable.
SuperblockSuperblock (xfs_info)Master map: block size, inode count, UUID, features, journal location. Has backups on ext4.
Block groupAllocation groupHow inodes/blocks are partitioned across the disk for locality and parallelism.
ExtentsB+tree extentsContiguous data-block ranges — how to follow a file to its real contents on disk.
crtime / btimecrtime / btimeTrue file birth time — NOT ctime. Hidden from plain stat; the trustworthy 'first appeared' timestamp.
Journal (ordered/writeback)Internal log (metadata-only)Replayed on mount → a read-write mount alters evidence. Use ro,noload / a write blocker.
dumpe2fs / debugfs / tune2fsxfs_info / xfs_db -rRead the internals. Wrong-filesystem tooling errors out — identify FSTYPE first.
fsck.ext4 -b <backup>xfs_repairRepair path. Both write to disk — only ever on an image/copy, never live evidence.