TL;DR
C is not creation, most reads leave no trace, and no Linux timestamp interface can set the birth time.
ext4 stores M, A and C, which POSIX defines, and B, which it does not: the kernel writes the birth time as crtime when the inode is created, and no userspace interface sets it. C is status change and not creation. On a default install relatime discards most access time updates, so an unchanged A is the expected result and not evidence that a file was not read. A file system formatted with 128 byte inodes has no birth time and only one second of resolution.
The timestamp interfaces reach A and M only and move C to now, which is why C earlier than M catches a file dated forward, and why every one of these properties assumes an honest clock. Each tell has a benign explanation: a move across file systems gives a fresh birth time with an inherited M, an editor that saves by replacing creates a new inode, and a file manager built on an older GNOME I/O library truncates to microseconds. The behavior comes from Thierry and Müller's DFRWS EU 2022 paper, rechecked on ext4 and XFS on five current systems. This note adds which of those facts change a conclusion in a report.
Download the cheatsheet:
Linux_timestamps_poster(ext4_xfs)_light.pdf Linux_timestamps_poster(ext4_xfs)_dark.pdf
We spend our careers on NTFS and then get handed a Linux image. The letters look familiar, the tooling prints four timestamps, and the temptation is to read them the way we read an $SI triple. That is where the findings go wrong.
This one is different from most of what I write here in one important way. The behavior I describe here is not mine. It comes from a paper I think every DFIR consultant working outside Windows should read, and I want to be explicit about that up front rather than absorb someone else’s work into a blog post. What I am adding is the part a paper does not do: which of these facts change a conclusion in a report, and which benign explanation will be waiting for you when you write it down.
Technical assumptions
Linux on ext4 with 256 byte inodes, the Ubuntu and Debian default, and on XFS, the Red Hat default. The two behave the same except where I say so. Where I say something is standard behavior rather than Linux behavior, I mean POSIX.
The behavioral findings, including the mount option rules, the interface limits and the editor patterns, are from Thierry and Müller, A systematic approach to understanding MACB timestamps on Unix-like systems, DFRWS EU 2022. Their test framework is open source and the paper is open access. Full citation at the end. In October 2026 I reran the operations that matter here on five systems: RHEL 9.4 (kernel 5.14), Ubuntu 24.04 (6.8), AlmaLinux 10.2 on the RHEL 10 kernel (6.12), and RHEL 10 and Ubuntu 24.04 under WSL (6.18), reading every value from the inode on the unmounted file system. Every operation the paper and I both tested gave the same result. Where tools have changed since 2022, the text says so, and the commands and output are in the last section.
I am deliberately not covering macOS or the BSDs. The paper does, but it tested HFS+ and macOS has since moved to APFS, and a stale claim about a file system nobody runs any more is worse than no claim. Where the BSD behavior makes a Linux point sharper I mention it and say so.
The four letters, and what they mean here
POSIX defines three timestamps. Not four.
M, last data modification
The file’s contents changed. This one behaves roughly the way you expect.
A, last data access
The file’s contents were read. Hold this one loosely. On a default Linux install most reads do not update it, for reasons in the next section.
C, last status change
This is the one that catches people out. It is not creation. It is the inode metadata changing: permissions, ownership, link count, and also the timestamps themselves. Every standard operation that updates M also updates C.
B, birth
Not in POSIX at all. ext4 stores it as crtime, and the kernel fills it, but for years there was no interface to read it and there is still no interface to set it.
If you take one thing from this article: C is status change, not creation, and B is the field the standard does not know about. Those two facts are the root of most Linux timestamp mistakes I see.
How they are stored
In the ext4 inode each timestamp is a 4 byte signed count of seconds since 1970. With a 256 byte inode there is a second 4 byte field per timestamp, where the low two bits extend the epoch past 2038 and the upper thirty bits carry nanoseconds. Eight bytes per timestamp, the same total an NTFS FILETIME uses, arranged completely differently and giving 1 nanosecond resolution rather than 100.
Resolution is not accuracy. The kernel normally stamps an inode from a clock that advances once per timer tick, so operations that land inside the same tick record the same value to the nanosecond. The tick is 1 millisecond on the stock RHEL 9, RHEL 10 and Ubuntu 24.04 kernels and 4 milliseconds under WSL. I created a file, set its M into the past and created a second file, 30 times over. Inside one program the two C values tied 25 to 30 times on every kernel. As three separate commands they tied 20 to 24 times under WSL and at most 7 times on the stock kernels, where the gaps came in whole milliseconds. Nanosecond digits order events that are a tick or more apart. Inside a tick they tie, and a tie says nothing about which came first. From Linux 6.13, ext4 and XFS also use multigrain timestamps: once a file’s M or C has been read, its next change takes a fine-grained clock value, so two changes inside one tick can differ if something looked at the file in between. A stays coarse. Of the kernels tested, only the 6.18 WSL kernel has it, and the ties above held there too.
That extra field is why inode size matters. Format a file system with 128 byte inodes and there is no room for it: you lose the nanoseconds, resolution drops to 1 second, and crtime does not exist at all. If you are staring at a Linux image wondering why every birth time is missing, check the inode size before you build a theory.
Access time is mostly not there
This is the single biggest difference from Windows work, and the one I would put in the first paragraph of a Linux triage note.
Linux mounts file systems with relatime by default. Under that rule the access time is only written if the existing A was already earlier than or equal to M or C, or if it is more than a day old. Read a file twice in an afternoon and the second read leaves nothing. The paper is blunt about the consequence: this makes a default Linux install non-compliant with POSIX, and it is accepted because the performance cost of honest access times is not worth paying.
So on a default system, an unchanged access time is not evidence that a file was not read. It is the expected result.
On XFS there is one more case. A plain cp there shares the original’s blocks instead of reading them, so the source’s A stays unchanged although the file was copied. That held on every kernel tested; with --reflink=never, A moved as on ext4.
False positives and false comfort. The mount options are per file system, so check each one rather than assuming. noatime means ordinary reads never update A. The field keeps whatever was last written to it, which is the creation time unless something set it explicitly: a copy that preserves times, an archive extraction, or touch. Either way it says nothing about reads. nodiratime does the same for directories only. strictatime restores standard behavior, and if you find it set deliberately on a server, someone made a choice you should ask about. There is also lazytime, which decides when timestamp updates reach the disk rather than whether they happen. Under it the current times live only in the in-memory inode, and the on-disk inode catches up when something else about the inode changes, when an application syncs, when the inode leaves memory, or after 24 hours. That has two consequences for evidence. On a live system, stat reads memory and debugfs reads the disk, so the two can disagree for recently touched files. And an image of a machine that crashed or lost power can hold older timestamps than the machine was showing. Record which of the two views each timestamp came from. The paper does not cover lazytime; this comes from the mount documentation linked at the end.
The asymmetry, and it is the opposite of the BSDs
On NTFS the useful asymmetry is that the same four times are stored twice and only one copy is reachable through the documented API. Linux has an asymmetry too, and it sits somewhere else entirely.
The interface for setting timestamps reaches the access and modification times. That is all. There is no interface that sets the status change time to a value of your choosing, and setting A or M updates C to now as a side effect. On Linux there is also no interface that sets the birth time. The kernel writes crtime when the inode is created and nothing in userspace will move it.
That gives you two fields with useful properties on Linux: C, which the timestamp interfaces can only move to the current time, and B, which they cannot move at all.
Now the part that matters if you work across platforms. On FreeBSD and macOS the equivalent interfaces do allow the birth time to be set, and the paper notes this is exposed to ordinary users through the standard command line tool. So “the birth time cannot be faked” is a Linux statement, not a Unix statement. Say it about the wrong platform in front of opposing counsel and you have handed them the afternoon.
And the standard caveat applies to all of it. C and B are written from the system clock, so everything above assumes the clock was honest. An attacker with root can set the clock back, create or change the file, and set the clock forward again, and C and B then hold whatever time they chose. That route leaves traces of its own: journal entries whose wall-clock times run backwards against their recorded order, and the correction chrony or ntpd reports if time sync pulls the clock back. With root and direct writes to the block device, every field is writable without touching the clock at all. These properties describe what the interfaces allow, not what an attacker with full control of the box is limited to.
The tells, and the benign explanation for each
Same structure as the NTFS article, because the same discipline applies. A tell you cannot argue against is not a finding.
1. C earlier than M
Every standard operation that changes content sets M and C to the same moment, and no timestamp interface sets C directly. So a status change time earlier than the modification time should not happen through ordinary use. The pair inverts in one of two ways: something set M into the future, or the clock moved backwards between the write and a later status change. On a test ext4 system, touch setting M to 2030 produced exactly this: M in 2030, C at the moment of the command.
That makes this the tell for a timestomp that dates a file forward, not back. Setting M into the past leaves C later than M, which is the same shape as every file that was ever chmodded, restored from an archive or installed from a package. In the same test, M set to 2020 left C at the moment of the command, and nothing in the pair looked wrong.
For backdating, the neighbors say more than the pair. A backdated M is only as precise as the date typed, so touch with a date and no fraction leaves the nanoseconds at exactly zero, as it did in the test. A single file with a whole-second M among files the same software wrote with full precision is the lead. Many archive and package formats also store whole seconds, so the lead holds only in a directory where they would not.
False positives. The comparison only holds if you trust the clock. A system time change, an NTP correction, or an image restored from a machine with a wrong clock will produce the same shape across a whole population of files. So will an archive extracted, or a copy made with preserved times, from a machine whose clock ran ahead; GNU tar warns about future timestamps when it extracts them. The warning goes to standard error and tar still exits with success, so when extraction runs inside a script or an installer, unless something captured that output, the file is the only record left. That population is the discriminator. One file with an odd pair is interesting, ten thousand of them is a clock event.
2. Birth time later than modification time
Superficially damning: the file claims to have been created after it was last written.
False positives. This is the one I would most expect to see misread, because it is the normal result of an entirely innocent operation. Moving a file between two file systems cannot be a rename, so the tooling copies it. Per the paper, on Linux the destination gets a fresh birth time while inheriting the modification time from the source. In the paper's tests, a file moved from the home partition to a different file system comes out looking like this. The same shape appears in restores from archives that preserve M and A but not creation. Measured on ext4 and XFS: M and A arrive intact to the nanosecond, and C and B are the time of the move.
3. A brand new birth time on a file that has existed for months
You have the file in an older backup or an older inventory, and its birth time is yesterday.
False positives. This is the best material in the paper for our purposes, and it is entirely benign. Editors do not agree on what saving means. Some write into the existing file, which leaves the inode alone and moves M and C. Others write a new file and rename it over the original, which produces a new inode with all four timestamps updated, a new birth time included. Both are normal. The paper profiles fifteen editors and finds all three patterns represented, and at least one editor changes behavior depending on a single setting, so “which editor” is not even a stable answer on its own. Measured on ext4 and XFS: vim with its shipped defaults and sed -i produce a new inode, and nano writes in place. Start vim in Vi-compatible mode and it writes in place too.
The practical consequence: on Linux, a birth time tells you when this inode appeared, not when this file first existed. Treat it as the age of the container, not the age of the content.
4. Sub-second precision that looks wrong
On NTFS, timestamps ending in too many zeros usually mean an archive or a tool with fewer bytes to spend rather than tampering. Linux has its own version of that trap, and it is more useful, because it points at the tool.
ext4 gives you nanoseconds. A plain command line copy preserves that. But the paper found that copies made through the GNOME I/O library, which is what several graphical file managers use, came out with the modification and access times truncated to microseconds, the result of a bug first reported in 2010. GLib fixed it in version 2.74 in 2022. On RHEL 9, which ships GLib 2.68 and is supported until 2032, a copy with gio copy -p still ends in three zeros. On Ubuntu 24.04 and RHEL 10, which ship 2.80, the nanoseconds survive. So the tell applies to older systems only, and it needs the GLib version of the machine that made the copy. Other file managers truncate differently.
False positives. Do not run this the other way round. Microsecond precision does not prove a file manager was used, it is consistent with one. Plenty of other software truncates, and anything that has passed through an archive, a container image or a network file system may have lost precision on the way. This is a lead for attribution, not a conclusion.
5. A local move leaves almost nothing
Renaming a file, or moving it within the same file system, keeps the same inode. M, A and B are untouched and only C moves.
False positives. The trap is the reverse inference. Because a local move updates only C, a lone C change is consistent with a rename, with a permission change, with an ownership change and with the side effect of a timestomp. It narrows nothing on its own. You need the journal or another artifact to say which.
What none of it shows
Timestamps tell you that an operation touched a file. They do not tell you who, they do not tell you which process, and on Linux they frequently do not tell you that a read happened at all.
They are also the weakest evidence in the room the moment you have anything better. Journals, audit logs, the shell history, package manager records and the systemd journal all carry attribution that inode metadata never will. I reach for timestamps to build the shape of an incident and to test a hypothesis, not to prove one on their own.
Reproduce it
Everything above is checkable in about ten minutes on a spare virtual machine, and I would rather you did that than took my word or anyone else’s. Look at the mount options in effect, read all four timestamps including birth, then read the same inode straight from the file system and compare. If the mount uses lazytime, expect recently touched files to disagree. Set a timestamp into the future with the standard tool and watch the status change time stay at now. Save the same file in two different editors and compare the inode numbers before and after.
Read the values from the inode, not from stat, which shows the kernel’s copy in memory. The excerpts below are from RHEL 9.4, kernel 5.14, on an ext4 loop image mounted with the default relatime, with debugfs reading the unmounted image after each step. In each timestamp the first hex value is seconds since 1970 and the second is the extra field: shift it right by two bits for nanoseconds.
Dating a file forward: touch -d '2030-01-01 00:00:00' report.txt
-- before
Inode: 12
ctime: 0x6ac0fbe0:573fb8c8 -- Sat Oct 3 12:58:08 2026
atime: 0x6ac0fbe0:573fb8c8 -- Sat Oct 3 12:58:08 2026
mtime: 0x6ac0fbe0:573fb8c8 -- Sat Oct 3 12:58:08 2026
crtime: 0x6ac0fbe0:573fb8c8 -- Sat Oct 3 12:58:08 2026
-- after
Inode: 12
ctime: 0x6ac0fbe1:69220670 -- Sat Oct 3 12:58:09 2026
atime: 0x70dbd880:00000000 -- Tue Jan 1 00:00:00 2030
mtime: 0x70dbd880:00000000 -- Tue Jan 1 00:00:00 2030
crtime: 0x6ac0fbe0:573fb8c8 -- Sat Oct 3 12:58:08 2026
M and A take the date typed, with zero nanoseconds. C takes the moment of the command, so C is now earlier than M. B does not move.
Saving in vim with its shipped defaults
-- before
Inode: 13
mtime: 0x6ac0fbe1:750dd060 -- Sat Oct 3 12:58:09 2026
crtime: 0x6ac0fbe1:750dd060 -- Sat Oct 3 12:58:09 2026
-- after
Inode: 14
mtime: 0x6ac0fbe2:8952799c -- Sat Oct 3 12:58:10 2026
crtime: 0x6ac0fbe2:8952799c -- Sat Oct 3 12:58:10 2026
A new inode under the same name, with a birth time equal to the save.
gio copy -p on GLib 2.68
-- source
mtime: 0x6ac0fbe2:9d597af0 -- Sat Oct 3 12:58:10 2026
-- copy
mtime: 0x6ac0fbe2:9d596ee0 -- Sat Oct 3 12:58:10 2026
Decoded, the source M is .659971772 seconds and the copy’s is .659971000: the last three digits are gone.
The commands you want are findmnt or mount for the options actually in effect, stat for the four timestamps, statx if your tooling is old enough that stat will not print birth, debugfs with the stat command against the inode number for the on-disk view of ext4, xfs_db in read-only mode for XFS, dumpe2fs or xfs_info for the inode format, and touch with an explicit date for the timestomp test. If your stat shows a birth time of dashes, check the inode size before concluding anything. The test harness behind the measurements in this note is available on request.
Putting it together
If I am handed a Linux file system and asked whether the timestamps can be trusted, the order is roughly this. Establish the mount options, because they decide whether access time means anything at all. Establish the inode size, because it decides whether birth time exists. Then treat C and B as the interesting pair, since no timestamp interface sets them to a chosen value and the only other route runs through the system clock, which leaves evidence of its own, and treat M and A as the two that are cheap to fake.
And then, before writing any of it down, ask the question that has saved me more than once: what is the boring explanation. A cross-volume move, an editor that replaces rather than overwrites, a file manager that truncates precision, a clock that was wrong. On Linux the boring explanation is available for almost every tell, which is exactly why the tells are worth knowing properly.
Credit where it belongs
The systematic work here is Thierry and Müller’s, not mine. They built a test framework, ran 187 compliance tests across four operating systems, profiled libraries and fifteen editors, and released the code. If your work touches Unix file systems at all, read the paper rather than my summary of it, and use their reference tables rather than anything I could reproduce here. It is open access under a license that does not permit derivatives, which is another good reason to send you to the source. The XFS results, the tick measurements and the GLib version split are mine; the rest is theirs.
Key takeaways
- C is status change, not creation, and B is not in POSIX. Those two facts are the root of most Linux timestamp mistakes.
- Access time is mostly not there. relatime is the default, so an unchanged A is the expected result. Check the mount options of each file system: noatime, nodiratime, strictatime and lazytime each change what A means. On XFS, a plain cp leaves the source’s A untouched.
- Check the inode size before building a theory. A 128 byte inode has no room for the extra field: no nanoseconds, one second of resolution, and no crtime. A birth time of dashes is a formatting fact.
- lazytime splits the live view from the disk view. stat reads memory and debugfs reads the disk, so they can disagree for recently touched files, and an image of a crashed machine can hold older times than it was showing. Record which view each timestamp came from.
- No interface sets C or B on Linux. Setting A or M moves C to now. "The birth time cannot be faked" is a Linux statement and not a Unix one, because on FreeBSD and macOS the equivalent interfaces can set it.
- C earlier than M catches forward-dating, not backdating. For backdating, the neighbors say more: a whole-second M among files written with full precision. One file with an odd pair is a lead, and ten thousand of them is a clock event.
- Birth time is the age of the inode, not of the content. Editors that write a new file and rename it over the original, and moves across file systems, give a new birth time on an old file. Usually that is a save and not tampering.
- Precision can point at a tool without proving it. Microsecond truncation is consistent with a file manager built on GNOME I/O older than GLib 2.74, as shipped with RHEL 9, and plenty of other software, archives and network file systems truncate as well.
- Every property assumes an honest clock. An attacker with root can move the clock, and with direct block device writes can set any field without touching it. Look for journal entries whose times run backwards and for time sync corrections.
- Timestamps are the weakest evidence in the room. Journals, audit logs, shell history, package manager records and the systemd journal carry attribution that inode metadata never will. Use timestamps to build the shape of an incident and to test a hypothesis, and ask for the boring explanation before writing a tell down.
References
- Primary source A. Thierry and T. Müller, A systematic approach to understanding MACB timestamps on Unix-like systems, DFRWS EU 2022, Forensic Science International: Digital Investigation 40 (2022) 301338. Open access, CC BY-NC-ND 4.0 doi.org/10.1016/j.fsidi.2022.301338
- Test framework os_timestamps, the open source profiling and POSIX compliance framework from that paper github.com/QuoSecGmbH/os_timestamps
- Mount options relatime, strictatime, noatime, nodiratime and lazytime, as documented for mount man7.org/linux/man-pages/man8/mount.8.html
- Interfaces utimensat and futimens, the interfaces that set access and modification times and nothing else man7.org/linux/man-pages/man2/utimensat.2.html
- Interfaces statx, and the birth time flag that finally exposed crtime to userspace man7.org/linux/man-pages/man2/statx.2.html
- Timestamp semantics inode(7), on what the three POSIX timestamps mean and when each is updated man7.org/linux/man-pages/man7/inode.7.html
- On-disk layout The ext4 inode, including the extra timestamp fields that carry nanoseconds and the epoch bits kernel.org/doc/html/latest/filesystems/ext4/inodes.html
- Timestamp granularity Multigrain timestamps: fine-grained M and C after a query, coarse A; ext4 and XFS from Linux 6.13 docs.kernel.org/filesystems/multigrain-ts.html
- Copies GNU coreutils 9.0 release: cp makes a reflink copy by default lwn.net/Articles/870372
- Copies mkfs.xfs: reflink enabled by default on new XFS file systems man.archlinux.org/man/mkfs.xfs.8.en
- Copies GLib issue 369: file copy does not preserve nanosecond timestamps, reported 2010, fixed for 2.74 gitlab.gnome.org/GNOME/glib/-/issues/369
- Archives GNU tar warnings: timestamp warnings go to standard error and do not change the exit code gnu.org/software/tar/manual/html_section/warnings.html
- Editors vim backupcopy: when a save renames a new file over the original and when it writes in place vimhelp.org/options.txt.html#%27backupcopy%27
- Support life Red Hat Enterprise Linux life cycle: RHEL 9 maintenance support to 31 May 2032 access.redhat.com/support/policy/updates/errata
- Companion piece The Windows half of this problem: what file timestamps mean, and how to catch tampering /field-notes/what-file-timestamps-mean/