You are currently viewing Finding File Data in a Disk Image from an XFS Inode

Finding File Data in a Disk Image from an XFS Inode

XFS is the default filesystem for Red Hat Enterprise Linux and much of the cloud, yet commercial forensic tools still offer only patchy support for it. When a case turns on proving exactly where a file’s contents sit inside a disk image, examiners need to be able to walk from an XFS inode to a byte offset by hand. This post shows the full conversion, then introduces a small script that does the arithmetic for you — but only after you understand what the arithmetic is.

Why decode this manually?

Three reasons. First, tool coverage: The Sleuth Kit and most commercial suites handle EXT well but stumble on XFS structures, so manual verification is often the only way to confirm a finding. Second, evidence: if you extract file content at a specific offset, you should be able to demonstrate in a report precisely how that offset was derived. Third, recovery: when a file is deleted in XFS the directory entry is marked unused but the extent data in the inode can survive, so decoding extents by hand is sometimes the difference between recovering content and giving up.

Where the file address lives in the inode

An XFS version 3 inode (the type used on v5 filesystems) starts with a 176-byte core structure. Two fields matter here:

  • Byte 5 — data fork type. A value of 2 means the inode holds an array of extent records; this covers most ordinary files. (A value of 1 means the data is resident in the inode itself, and 3 means the fork holds the root of a B+tree, typical for very large or heavily fragmented files. The method below applies to type 2.)
  • Bytes 76–79 — extent count. How many extent records follow the core.

The extent records themselves begin at byte offset 176, immediately after the core, and each one is exactly 16 bytes.

Decoding the XFS inode extent record

This is where it gets awkward. The 16-byte (128-bit) extent record is bit-packed, not byte-aligned, so you cannot simply read fields with a hex viewer. The layout is:

  • Flag — 1 bit (set if the extent is preallocated/unwritten)
  • Logical offset — 54 bits (position of this extent within the file)
  • Start block — 52 bits (the filesystem block where the data begins)
  • Block count — 21 bits (length of the extent in blocks)

Because 1 + 54 = 55 bits, the start block field begins partway through a byte. The only reliable way to read it is to convert the whole record to binary, count off the fields, and convert the pieces back.

A worked example

Take this extent record, read from byte 176 of an inode:

00 00 00 00 00 00 00 00 00 00 00 35 3F 40 00 01

Converted to binary and split into the four fields:

Flag:           0
Logical offset: 000000000000000000000000000000000000000000000000000000
Start block:    0000000000000000000000000000000000011010100111111010
Block count:    000000000000000000001

The flag is clear and the logical offset is zero — this extent is the start of the file. The block count is 1, so the file occupies a single block. Reading the start block bits back into hex: 0001 1010 1001 1111 1010 is 0x1A9FA.

That is a block address, not a byte address. Multiply by the filesystem block size (4,096 bytes here) to get the offset into the image:

0x1A9FA × 0x1000 = 0x1A9FA000 (446,668,800 decimal)

The file’s data begins 446,668,800 bytes into the filesystem. You can now extract it directly:

dd if=image.dd bs=4096 skip=$((0x1A9FA)) count=1 of=recovered.bin

or simply confirm what is there with xxd -s 446668800 image.dd | head.

A script to do the bit-shuffling

Converting 128 bits by hand is error-prone at 2 a.m. during an incident, so there is a helper script available: xfs_inode_converter.sh.

It is deliberately simple, and it is worth being clear about exactly what it does:

  1. It takes the 16-byte extent record as its only argument — 32 hex digits, no spaces (for the example above: 0000000000000000000000353F400001).
  2. It converts the hex to binary using a character lookup table rather than bc (earlier versions used bc, which produced erratic output).
  3. It splits the binary string into the four fields at the 1, 54, 52, and 21-bit boundaries and prints each one, so you can check the working.
  4. It converts the 52-bit start block to decimal and hex.
  5. It multiplies the start block by 4,096 and reports the result as the byte offset into the filesystem, in both hex and decimal.

In other words, the script performs precisely the manual process described above — nothing more. That matters for two reasons. First, if the output is ever challenged, you can reproduce every step by hand. Second, its assumptions are your responsibility to verify:

  • Block size. The script assumes 4,096-byte blocks. Confirm yours from bytes 4–7 of the superblock (xfs_db -r -f image.dd -c "sb 0" -c "p blocksize") and scale the result if it differs.
  • One extent at a time. A fragmented file has multiple extent records; run the script against each 16-byte record and use the logical offset field to keep them in order.
  • Fork type. The input must come from a type 2 data fork. If byte 5 of the inode is 3, the bytes at offset 176 are a B+tree root, not an extent record, and the output will be meaningless.

Validating the result

Good practice is to cross-check the conversion with xfs_db, which can decode extents itself: open the image read-only, select the inode with inode <number>, and print the block map. The convert command will also translate between address types (filesystem block, allocation group block, byte offset) if you want independent confirmation of the arithmetic. On filesystems with multiple allocation groups, remember that the start block is a filesystem block number that encodes the allocation group in its high bits — another reason a quick xfs_db sanity check is worthwhile before you commit an offset to a report.

The technique is not complicated once the bit boundaries are understood — it is simply careful counting. Decode it by hand at least once; after that, let the script save you the eyestrain.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.