Reverse lookup of inode/file from offset in raw device on linux and ext3/4?

Solution 1:

I just had to do a similar thing, so I thought I'd share my solution.

You can see which partition a drive byte offset belongs to by checking the 'offset' and 'size' elements of the udisks --show-info output; e.g.

user@host:~$ sudo udisks --show-info /dev/sda1 | grep -i 'offset'
    offset:                    1048576
    alignment offset:          0

Subtract this offset from the disk offset to get the byte offset into the partition. So disk offset (10000000) in /dev/sda is partition offset (10000000 - 1048576) = 8951424 in /dev/sda1

You can find out how large blocks are in a partition using the following command:

user@host:~$ sudo tune2fs -l /dev/sda1  | grep -i 'block size'
Block size:               4096

Divide the partition byte offset by the block size to determine the block offset, in this case 8951424 / 4096 = 2185

Run the following command to find out what inode occupies that block:

user@host:~$ sudo debugfs -R "icheck 2185" /dev/sda1
debugfs 1.41.11 (14-Mar-2010)
Block   Inode number
2185    123456 

then the following command to find out what the filename is for that inode:

user@host:~$ sudo debugfs -R "ncheck 123456" /dev/sda1
debugfs 1.41.11 (14-Mar-2010)
Inode   Pathname
123456  /tmp/some-filename.txt

There's a longer description of how this at http://www.randomnoun.com/wp/2013/09/12/determining-the-file-at-a-specific-vmdk-offset

Solution 2:

Greg Knox's answer is correct, but could be easier. I've written a shell script, lba2file, which performs all the arithmetic for you, source code below.

[Update: Script no longer depends on udisks binary].

Example usage of lba2file

Solving the problem posed in the question (with address specified in bytes):

kremvax$ sudo lba2file -b 1000000 /dev/sda
Disk Byte 1000000 is at filesystem block 124744 in /dev/sda1
Block is used by inode 21762939
Searching for filename(s)...
Inode           Pathname
21762939        /home/lilnjn/backups/adhumbla_pics_2.zip

Example usage with S.M.A.R.T.

If your hard drive has a bad sector, you may want to find out what file is corrupted before you remap the sector by writing zeros to it. You can do so easily using smartctl and lba2file.

kremvax$ sudo smartctl -C -t short /dev/sdd    
kremvax$ sudo smartctl -a /dev/sdd | grep '^# 1'
# 1  Short captive   Completed: read failure   90%   20444   1218783739

The final number 1218783739 is the disk address in sectors, not bytes:

kremvax$ sudo lba2file 1218783739 /dev/sdd
Disk Sector 1218783739 is at filesystem block 152347711 in /dev/sdd1
Block is used by inode 31219834
Searching for filename(s)...
Inode           Pathname
31219834        /home/mryuk/2020-11-03-3045-us-la-msy.jpg
31219834        /home/mryuk/web/2020-11-03-3045-us-la-msy.jpg

Discussion

My script defaults to a sector address (often called "LBA") rather than bytes. This is because LBA is what tools like smartctl will report when there is a bad block on the drive. However, if you want to specify bytes instead of sectors, just give the -b flag.

Source Code

Cut and paste into a file or click here to download from https://github.com/hackerb9/lba2file/

#!/bin/bash

# lba2file: Given an LBA number and a drive in /dev/, print which
# filename(s), if any, use that sector.

# This is the opposite of `hdparm --fibmap /foo/bar`

# B9 May 2020

if [[ "$1" == "-b" ]]; then
    BYTESFLAG=Byte
    shift
fi

if [[ $# -lt 2 ]]; then
    echo "Usage: lba2file  [-b]  <sector number>  /dev/sdX"
    echo "  -b: Use byte address instead of sector"
    exit 1
fi

if [[ $(id -u) -ne 0 ]]; then
    echo "Please run as root using 'sudo $@'" >&2
    exit 1
fi

lba=$1
drive=$2
drive=${drive#/dev/}        # Remove /dev/ prefix, if any.

if [[ "$drive" =~ ^(.*)[0-9]$ ]]; then    # Either user specified a partition.
    searchparts="/sys/class/block/$drive"
    drive=${BASH_REMATCH[1]}
else                     # Or user specified a drive.
    shopt -s nullglob            # Don't use '?' literally.
    searchparts=$(eval echo /sys/class/block/${drive}?)
fi

for partition in $searchparts; do
    device=/dev/${partition#/sys/class/block/}
    cd "$partition" || continue
    start=$(cat "$partition/start")
    partitionsize=$(cat "$partition/size")
    hwsectorsize=$(cat "/sys/class/block/$drive/queue/hw_sector_size")

    # Typically: e2blocksize==4096, hwsectorsize==512
    # Example: start=1048576, partitionsize=640133980160
    # Do a sanity check.
    if [[ -z "$start" || -z "$partitionsize" || -z "$hwsectorsize" ]]; then
    echo "Error reading data for $device" >&2
    continue
    fi

    # Scale everything to bytes since we'll use that for debugfs.
    start=$((start * hwsectorsize))
    partitionsize=$((partitionsize * hwsectorsize))

    # If not using byte flag, scale the address, too.
    if [[ -z "$BYTESFLAG" ]]; then
    byteaddress=$((lba * hwsectorsize))
    else
    byteaddress=$lba
    fi
    if [[ $byteaddress -lt $start ||
      $byteaddress -ge $((start+partitionsize)) ]]; then
    #echo "Address $byteaddress is not within $partition"
    continue        # Not in this partition
    fi

    if ! e2blocksize=$(tune2fs -l $device 2>/dev/null |
               grep '^Block size' | egrep -o '[0-9]+'); then
    echo "Skipping $device, not an Ext2/3/4 partition" 
    continue
    fi

    # Scale address by filesystem blocksize to find filesystem block number
    e2blockaddress=$(( (byteaddress - start) / e2blocksize))

    Sector=${BYTESFLAG:-Sector}
    echo "Disk $Sector $lba is at filesystem block $e2blockaddress in $device"
    inode=$(debugfs -R "icheck $e2blockaddress" $device 2>/dev/null |
           tail -1 | cut -f2)
    if [[ "$inode" && "$inode" != "<block not found>" ]]; then
    echo "$Sector is used by inode $inode"
    echo "Searching for filename(s)..."
    debugfs -R "ncheck $inode" $device 2>/dev/null
    else
    echo "$Sector is not in use."
    fi
done