File creation time on Linux

2 people like this post.

What if, on Linux, we wanted to know the creation time of a file? The stat command (and the system call it uses) returns three timestamps: access, modify and change. None of those is really what we are looking for; they just tell us about the last time a file’s contents (or metadata) have been read/written.

Could it be that the filesystem stores more information than what the standard POSIX interface can access? The ext4 inode structure contains the i_crtime field, which is exactly what we are after: the file creation time. This structure is only valid for ext4 filesystems formatted with “large” (256 bytes) inodes. The inode size used in previous version of ext (128 bytes) can’t accommodate any extra metadata.

How can we obtain the file creation time, then? It seems that the only user space tool able to do so is debugfs:

vagrant@muffin:~$ stat x.txt
  File: `x.txt'
  Size: 2               Blocks: 8          IO Block: 4096   regular file
Device: fc00h/64512d    Inode: 2885160     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1000/ vagrant)   Gid: ( 1000/ vagrant)
Access: 2013-04-25 19:34:22.923399413 +0000
Modify: 2013-04-25 19:34:06.940015907 +0000
Change: 2013-04-25 19:34:06.940015907 +0000
 Birth: -
vagrant@muffin:~$ sudo debugfs -R "stat <$(stat -c '%i' x.txt)>" /dev/mapper/precise64-root | grep crtime
debugfs 1.42 (29-Nov-2011)
crtime: 0x517984fc:9a020d64 -- Thu Apr 25 19:33:16 2013

debugfs is built around libext2fs. We can use that ourselves to read the entire contents of an inode and extract crtime from there. In debugfs, the do_stat function does just that. It involves only two calls into libext2fs: ext2fs_open and ext2fs_read_inode_full.

I decided to try and interface Ruby with libext2fs and the FFI gem made that possible without me having to write a single line of C code… See below (or on Github) for a working example.

vagrant@muffin:~$ sudo ruby crtime.rb /dev/mapper/precise64-root x.txt
crtime: 0x517984fc:9a020d64 -- Thu Apr 25 19:33:16 +0000 2013
# Given a device and a file, returns the file creation time (crtime).
# Works only on ext4 filesystem with 256 bytes inodes.
# Requires libext2fs (apt-get install e2fslibs) and root access.
# See:
#      http://www.108.bz/posts/it/file-creation-time-on-linux/
#      http://computer-forensics.sans.org/blog/2011/03/14/digital-forensics-understanding-ext4-part-2-timestamps
# -- giuliano@108.bz

require 'rubygems'
require 'ffi'

module Ext2Fs

    # See: /usr/include/ext2fs/ext2fs.h

    extend FFI::Library
    ffi_lib '/lib/x86_64-linux-gnu/libext2fs.so.2'

    EXT2_FLAG_64BITS            = 0x20000
    EXT2_NDIR_BLOCKS            = 12
    EXT2_IND_BLOCK              = EXT2_NDIR_BLOCKS
    EXT2_DIND_BLOCK             = (EXT2_IND_BLOCK + 1)
    EXT2_TIND_BLOCK             = (EXT2_DIND_BLOCK + 1)
    EXT2_N_BLOCKS               = (EXT2_TIND_BLOCK + 1)

    typedef :long,    :errcode_t
    typedef :pointer, :io_manager
    typedef :pointer, :ext2_filsys
    typedef :pointer, :ext2_filsys_ptr
    typedef :pointer, :ext2_inode
    typedef :pointer, :struct_ext2_inode_ptr
    typedef :uint32,  :ext2_ino_t

    attach_variable :unix_io_manager, :unix_io_manager, :pointer;

    # Top portion of struct_ext2_filsys
    class Ext2FilsysAbridged < FFI::Struct
        layout :magic,       :errcode_t,
               :io,          :pointer,
               :flags,       :int,
               :device_name, :string,
               :super,       :pointer,
               :blocksize,   :uint

    class Ext2InodeLarge_linux1 < FFI::Struct
        layout :l_i_version, :uint32
    class Ext2InodeLarge_hurd1 < FFI::Struct
        layout :h_i_translator, :uint32
    class Ext2InodeLarge_osd1 < FFI::Union
        layout :linux1, Ext2InodeLarge_linux1,
               :hurd1,  Ext2InodeLarge_hurd1

    class Ext2InodeLarge_linux2 < FFI::Struct
        layout :l_i_blocks_hi,     :uint16,
               :l_i_file_acl_high, :uint16,
               :l_i_uid_high,      :uint16,
               :l_i_gid_high,      :uint16,
               :l_i_checksum_lo,   :uint16,
               :l_i_reserved,      :uint16
    class Ext2InodeLarge_hurd2 < FFI::Struct
        layout :h_i_frag,      :uint8,
               :h_i_fsize,     :uint8,
               :h_i_mode_high, :uint16,
               :h_i_uid_high,  :uint16,
               :h_i_gid_high,  :uint16,
               :h_i_author,    :uint32
    class Ext2InodeLarge_osd2 < FFI::Union
        layout :linux2, Ext2InodeLarge_linux2,
               :hurd2,  Ext2InodeLarge_hurd2

    class Ext2InodeLarge < FFI::Struct
        layout :i_mode,         :uint16,
               :i_uid,          :uint16,
               :i_size,         :uint32,
               :i_atime,        :uint32,
               :i_ctime,        :uint32,
               :i_mtime,        :uint32,
               :i_dtime,        :uint32,
               :i_gid,          :uint16,
               :i_links_count,  :uint16,
               :i_blocks,       :uint32,
               :i_flags,        :uint32,
               :osd1,           Ext2InodeLarge_osd1,
               :i_block,        [:uint32, EXT2_N_BLOCKS],
               :i_generation,   :uint32,
               :i_file_acl,     :uint32,
               :i_size_high,    :uint32,
               :i_fadd,         :uint32,
               :osd2,           Ext2InodeLarge_osd2,
               :i_extra_isize,  :uint16,
               :i_checksum_hi,  :uint16,
               :i_ctime_extra,  :uint32,
               :i_mtime_extra,  :uint32,
               :i_atime_extra,  :uint32,
               :i_crtime,       :uint32,
               :i_crtime_extra, :uint32,
               :i_version_hi,   :uint32

    # extern errcode_t ext2fs_open(const char *name, int flags, int superblock,
    #                unsigned int block_size, io_manager manager,
    #                ext2_filsys *ret_fs);
    attach_function :ext2fs_open, [:string, :int, :int, :uint, :io_manager, :ext2_filsys_ptr], :errcode_t

    # extern errcode_t ext2fs_read_inode_full(ext2_filsys fs, ext2_ino_t ino,
    #                   struct ext2_inode * inode,
    #                   int bufsize);
    attach_function :ext2fs_read_inode_full, [:ext2_filsys, :ext2_ino_t, :struct_ext2_inode_ptr, :int], :errcode_t


if !(ARGV.length == 2 && File.readable?(ARGV[0]) && File.readable?(ARGV[1]))
    puts <<-EOM
    Usage: $0 device_with_ext4_filesystem filename
      Make sure the device and the file are readable
    device   = ARGV[0]
    filename = ARGV[1]

current_fs_ptr = FFI::MemoryPointer.new :pointer
rc = Ext2Fs.ext2fs_open device,
                        Ext2Fs::EXT2_FLAG_SOFTSUPP_FEATURES | Ext2Fs::EXT2_FLAG_64BITS,
                        0, 0,
                        Ext2Fs.unix_io_manager, current_fs_ptr
fail "Error #{rc} on ext2fs_open" if rc != 0
current_fs = Ext2Fs::Ext2FilsysAbridged.new current_fs_ptr.read_pointer

# This is quite fragile, I should also check s_rev_level in struct ext2_super_block
inode_size = current_fs[:super].read_array_of_uint16(INODE_SIZE_OFFSET+1)[INODE_SIZE_OFFSET]
fail "inode size is not 256 bytes" if inode_size != 256

inode_buf_ptr = FFI::MemoryPointer.new :char, inode_size
inode_number = File.stat(filename).ino
rc = Ext2Fs.ext2fs_read_inode_full current_fs.pointer, inode_number, inode_buf_ptr, inode_size
fail "Error #{rc} on ext2fs_read_inode_full" if rc != 0
inode = Ext2Fs::Ext2InodeLarge.new inode_buf_ptr

printf "crtime: 0x%08x:%08x -- %s\n", inode[:i_crtime], inode[:i_crtime_extra], Time.at(inode[:i_crtime]).to_s