Learn · File systems in hex

ext4

The default Linux file system. Names, metadata and data live in three separate places, joined by inode numbers and block numbers.

1 Blocks and block groups

ext4 divides the volume into blocks (1, 2 or 4 KiB) and groups them into block groups. Each group has its own block bitmap, inode bitmap and inode table, so that a file's metadata sits close to its data. This small sample uses 1 KiB blocks and has a single group:

Block 0boot area + superblock at byte 1024
Group descriptors+ reserved GDT blocks
Bitmapsblock, inode
Inode table256 bytes per inode
Data blocksjournal, directories, file contents
byte offset of block B = volume start + B × block size
What dumpe2fs -h and debugfs say about this volume

2 The superblock

The superblock is always at byte 1024 of the volume, whatever the block size, and is 1024 bytes long. Its magic number 53 EF sits at offset 0x38, i.e. byte 1080 (0x438) of the volume. It has the sizes you need for every calculation, plus identity information: UUID, label, the directory it was last mounted on, and timestamps.

Feature flags matter. extent, 64bit and flex_bg mean ext4; has_journal without them means ext3; neither means ext2. With metadata_csum every metadata structure carries a CRC32C, so a careless hex edit is detectable.

3 Group descriptors

The group descriptor table starts in the block after the superblock (here block 2, byte 0x800). One descriptor per group (64 bytes with the 64bit feature) gives the block numbers of the group's bitmaps and inode table.

4 Inodes

An inode holds everything about a file except its name: type and permissions, owner, size, four timestamps, and where the data is. Inodes are numbered from 1. Inode 2 is always the root directory.

group = (inode − 1) ÷ inodes per group (integer division) index = (inode − 1) mod inodes per group offset = inode table block × block size + index × inode size inode 2: group 0, index 1 → 66 × 1024 + 1 × 256 = 0x10900

Extents

ext4 describes the data with extents stored in the 60-byte i_block area: a 12-byte header (magic 0A F3) and up to four 12-byte entries, each saying "file block n, length l, starts at disk block b". Larger, fragmented files get an extent tree with index blocks. Here is photos/cat.raw (inode 14), 5,000 bytes in one extent of 5 blocks:

The small text file notes.txt (inode 12) needs a single block. Follow its extent to the data:

Four timestamps. mtime is the content, ctime the inode (changed by chmod, rename and deletion, and not settable with touch), atime the last read (often lazily updated), crtime the creation (ext4 only). All are UTC seconds since 1970, with nanoseconds in the extra fields.

5 Directories

A directory's data blocks contain variable-length entries: inode number, record length (distance to the next entry), name length, file type and the name. The name is the only link between a path and its inode.

Deleted entries survive in slack

When a name is removed, ext4 doesn't erase it. It adds the entry's record length to the previous entry, so directory walks skip it, but the bytes are still there. In the photos directory below, the entry for cat.raw now has a much larger record length than its name needs, and the old entry for draft.txt is hidden inside it:

The name points to inode 15. That inode is marked deleted: links 0, a deletion time, size 0, and an extent header with 0 entries. ext4 clears the extents on deletion, so the inode no longer says where the data was.

Where ext4 recovery comes from. With the extents gone, recovery relies on other copies of the metadata: the journal (inode 8) often still holds an older copy of the inode's block, with intact extents. Without that, only content-based carving remains.

6 Try it yourself

Answer from the hex views above. Numbers can be typed in decimal or hex (0x…).

At which byte offset of the volume does inode 12 (notes.txt) start?

Use the inode formula from section 4. The inode table block is in the group descriptor (+0x08), the inode size in the superblock (+0x58).

Inode table block: 42 00 00 00 = 66. Inode size: 00 01 = 256. Inodes per group: 00 04 00 00 = 1,024, so inode 12 is in group 0 with index 12 − 1 = 11.

66 × 1024 + 11 × 256 = 0x10800 + 0xB00 = 0x11300. The notes.txt inode view above starts exactly there.

At which block number does the data of photos/cat.raw (inode 14) start?

The first extent entry starts at +0x34 of the inode. Its start block is 48 bits: a 2-byte high part at +0x3A, then a 4-byte low part at +0x3C.

The 6 bytes are 00 00 34 05 00 00. High part 00 00 = 0, low part 34 05 00 00 = 0x534.

Start block = 0 × 232 + 0x534 = 1,332, i.e. byte 1,332 × 1024 = 0x14D000.

At which offset inside the photos directory block does the hidden draft.txt entry start?

An entry needs 8 bytes of header plus its name, rounded up to a multiple of 4. Find where cat.raw's entry starts and how much of it is really needed.

The cat.raw entry starts at +0x18 and has a 7-byte name: 8 + 7 = 15, rounded up to 16 bytes. Its record length DC 03 = 988 is far more than that.

0x18 + 16 = 0x28. There you find inode 0F 00 00 00 (15), record length CC 03 and the name draft.txt.

Which field of inode 15 records when the file was deleted?

Only one of the four timestamps is 0 in every file that still exists.

The deletion time at +0x14 is D8 F6 A7 6A = 0x6AA7F6D8 = 2026-09-14 13:30:00 UTC. In a live inode this field is 00 00 00 00.

ctime has the same value, because deletion also changes the inode. But ctime changes for many other reasons (chmod, rename), so only dtime says "deleted".

These are the extent header and the first extent of cat.raw (inode 14). Edit them and watch the decoded inode below:

  • Change the extent length at +0x10 (05 00) to 03 00. The extent now covers 3,072 bytes, less than the 5,000-byte file size.
  • Change the low start block 34 05 to 32 05. Which file's block does the extent point to now?
  • Change the magic 0A F3 to 00 F3. A tool can no longer trust anything in i_block.