Unix/Linux File Creation Stamps (aka birthtime), and NodeJS

  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    Sep. 23, 2019
    Last Updated:
    Sep. 21, 2019
  • classTags
    classClick for Tags

This learning experience started off because of a test failing on Travis-CI.

This test used NodeJS “fs” (“filesystem”), or more accurately, “fs-extra”, which extends the “fs” base. It simply created some files, waited a few seconds, modified one of the files to update its modification timestamp (mtime), and then checked to make sure that FS showed that the modification time minus the creation time was the span of the delay. This test passed with no issues on Node v12 and Node v10, but was failing 100% of the time on Node v8. Another very important fact (and clue to the real issue) was that Travis-CI uses Linux, whereas on my own PC, which runs Windows, the test passed on all versions, including Node v8.

The exact issue:

After adding some logs to my test, it became very clear what was causing the test to fail:

Diff between created and modified should have been 8, but was 0.

In this case, my test had waited 8 seconds to modify the file, so the difference between created time (stats.birthtime) and modification time (stats.mtime) should have been 8 seconds, but it was zero, indicating that modification time was identical to birthtime.

Since this issue was only appearing on Linux, I fired up a Ubuntu VM with the same version as Travis (16.04.6 LTS Xenial) to see what the actual OS had to say about this. The results were similarly disturbing:

Linux Ubuntu - File Stat Birthtime Unavailable

Here you can see that Ubuntu, and more precisely the stat system call, is refusing to show the creation time of a file that it just created.

So obviously NodeJS is not finding a birthtime from the OS, and just using the modified stamp as a fallback.

But why is birthtime showing as blank? Some quick searches made it sound like getting a file birthtime is either impossible on Unix, or it requires some insane workarounds. However, knowing that my test was passing on later versions of Node, on the same exact Ubuntu OS, obviously Node had found some sort of solution. What was it?

NodeJS fs.stat change and libuv

First, to prove that I’m not making this up, here is proof that in older versions of Node, prior to 10.16, fs.stat returns the wrong birthtime, whereas in 10.16 and up, it returns the right one:

NodeJS - Different FS Stat Birthtimes on Different Versions - GS

So, how the heck did Node fix this? The answer took some digging, but also gives some insight into how Node versioning and core dependencies work.

When I first tried to look through Node’s source code to find how “fs” worked, I wasn’t finding much of value. It turns out, that is because Node uses something called libuv for a lot of the heavy OS stuff, such as filesystem calls, and most of the code for FS stuff is in there.

Libuv and statx()

Knowing that I should be looking at libuv, I was finally able to get somewhere and found a proposal that explained how the libuv team found out about updates to the Linux system that would improve stat calls. This was primarily the introduction of the statx() syscall, which as opposed to stat, actually *does* return the birthtime!

The statx() syscall support was added to the Linux kernel in 4.11 (2017). It was then added to glibc v2.28 in 2018. glibc is the “Gnu C Library”, essentially a wrapper around low-level system stuff that allows you to make calls from C/C++. This is important, because this is how libuv makes system calls from its C powered codebase, and getting statx support in glibc meant that it could be used in libuv, which also meant it could get used in Node!

The proposal to use statx in libuv was accepted, and the code to do it was merged with PR #2184, in Feb 2019, which shortly after made it into the official v1.27.0 (Stable) release in March 2019 (see changelog). This  update to libuv, in turn, was manually pulled into the Node source code via this specific commit, on March 16th, 2019.

Here is where it gets a little confusing. That commit is tagged as “v12.10”, so you might expect that the tag, plus how recently it was merged, means that the improved fs.stat code is only in Node V12.10 and up… but you would be wrong (like I was).

Node versioning is a little complicated (see this and this), but the basic summary is that the libuv update was applied to multiple versions of Node based on where they were in their lifecycle:

  • Node v8.x did not receive the update on any releases, even though it is LTS, since it is on “maintenance LTS” and not “active LTS”
  • Node v9.x did not receive the update on any releases, since it is odd numbered, and thus is not LTS and the update did not happen during its active dev stage
  • Node v10.x received it, starting with 10.16.0, since it is “active LTS”
  • Node v12.x received it on all versions since it is under current development

Testing for statx support

So, back to my original issue, now that I know all the above, how do I detect when the test is going to run on a Linux system where Node’s fs.stat function will NOT use the improved statx calls instead of stat?

The pseudo code is something like this:

(node v < 10.16.0 && linux) OR (node v >= 10.16.0 && linux && kernel < 4.11)

If the above condition is found to be true in my code, I have Travis-CI simply skip the test that relies on an accurate birthtime. You can see how I implemented it if you look at the commits linked to from this issue.  No more failing tests! 🙂

Implementing yourself

If you are looking to implement the use of statx in your own program, outside of using it through NodeJS, you have some considerable work set out for you. I would recommend looking at how libuv brought in the changes in their PR, and also taking a look at this StackExchange answer.

One thought on “Unix/Linux File Creation Stamps (aka birthtime), and NodeJS”

  1. Duncan says:

    I ran into a very similar issue and Google sent me here… a superb writeup that saved loads of investigation. Thanks!

Leave a Reply

Your email address will not be published.