DEV Community

Maciej Toporowicz
Maciej Toporowicz

Posted on • Originally published at toporowicz.it

Fake the system clock for a single application with libfaketime

A bit of a back story

My use case was quite specific - I wanted to conduct some end-to-end tests of a Java app running in a Docker container based on CentOS. The piece of code that I wanted to test was relying on date comparison:

if (happenedYesterday(event)) {
    foo();
} else {
    bar();
}

Given that I was not able to modify the test data, the easiest thing was to somehow make the application think it's yesterday, make it create theevent, then restore the original date and make the application invoke the above piece of code.

The setup

Since I would like to show how I got from an idea to a working solution, I need to provide a way to reproduce all the mistakes that I've made as part of this exercise - I need an environment which is as close to the original as possible. To achieve it and also to keep the examples as simple as possible, I'll use fabric8/java-centos-openjdk8-jdk Docker image.

Let's start a container from an unmodified image and open up its shell:

[me@pc ~]$ docker run --name centos -d -it fabric8/java-centos-openjdk8-jdk /bin/bash
...
[me@pc ~]$ docker exec -u 0 -it centos /bin/bash
[root@centos /]#

NOTE: -u 0 argument makes the command log into the container as root (0 is root's user id)

In all the examples in this post, I'll use pc as in [me@pc ~]$ to indicate commands invoked on my local machine and centos as in [root@centos /]# for commands invoked inside the container.

Setting the system date in Docker - naive approach

In my naive approach, I thought this step would be as simple as running one of these commands:

[root@centos /]# date -s "15 Oct 2019 19:05"
date: cannot set date: Operation not permitted
Tue Oct 15 19:05:00 UTC 2019
[root@centos /]# hwclock --set --date "15 Oct 2019 19:05"
hwclock: Cannot access the Hardware Clock via any known method.
hwclock: Use the --debug option to see the details of our search for an access method.

Unfortunately, it wasn't.

I tried to find some workarounds, but as far as I understand, Docker reuses the clock of the host machine, so overriding the date in the container is either not doable or not easily doable123. As I'm just a casual user of Docker, I didn't want to dig deeper. However, when looking for the workarounds, I stumbled upon a different way to change the time - libfaketime.

libfaketime

libfaketime is a library which is able to 'override' system calls that applications use to retrieve current date or time. It is then able to provide a fake value for these calls. What's more, you don't have to change a line of your existing code or add it to your app's dependency list - it'll be transparent. Since I'm not a Linux guru, using this library at first felt like it was a wrapper for my java application, though it's not how it works.

The installation is straightforward - you grab the source code of the library and run make install in the root directory of the checked out sources. It'll result in a bunch of files getting created in /usr/local/lib/faketime. To automate this process, I created the following Dockerfile:

FROM fabric8/java-centos-openjdk8-jdk

USER root

RUN yum -y groupinstall 'Development Tools' && \
    yum -y install make unzip wget && \
    mkdir faketime && \
    cd faketime && \
    wget https://github.com/wolfcw/libfaketime/archive/master.zip && \
    unzip master.zip && \
    cd libfaketime-master && \
    make install

ENTRYPOINT /bin/bash

Now, it should be possible to start it all up and test the library:

[me@pc ~]$ docker build -t centos .
...
[me@pc ~]$ docker run --name centos -d -it centos /bin/bash
...
[me@pc ~]$ docker exec -u 0 -it centos /bin/bash
[root@centos /]# date
Wed Feb 19 17:16:34 UTC 2020
[root@centos /]# LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1 FAKETIME="-15d" date
Tue Feb  4 17:16:49 UTC 2020

With the path to libfaketime provided in the LD_PRELOAD variable, the FAKETIME variable set to -15 days, the invocation of date rendered a date 15 days in the past. To me, the most interesting bit is the LD_PRELOAD part.

LD_PRELOAD variable and the preloading mechanism

The LD_PRELOAD variable allows to specify paths to libraries which are to be loaded before any other libraries are loaded. What's important, all the symbols (e.g. functions) contained in the preloaded libraries take precedence over the symbols from libraries loaded afterwards456.

It means that if a program uses a function foo() from library A and library A is linked at runtime, it is possible to provide a path to library B containing a different implementation of foo() in the LD_PRELOAD variable. As a result, when the program references foo() in its source code, the implementation from library B will be invoked.

libfaketime replaces the symbols related to interactions with the system clock using the preloading mechanism.

Given all the knowledge gathered so far, it's time to fake the date in a Java app.

Setting a fake date for a Java app

First of all - to test the library, I created a simple application that prints the current date and time each second, forever. Here's the code:

import java.time.LocalDateTime;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {
  private static final int NO_DELAY = 0;

  public static void main(String[] args) {
    Runnable printCurrentDateTime = () -> System.out.println(LocalDateTime.now());

    Executors
      .newSingleThreadScheduledExecutor()
      .scheduleAtFixedRate(printCurrentDateTime, NO_DELAY, 1, TimeUnit.SECONDS);
  }
}

Then, to test the library with the application, I ran the CentOS container again with a volume set to the directory where the java class was.

[me@pc ~]$ docker run --name centos -v /home/test/:/app -d -it centos /bin/bash
...
[me@pc ~]$ docker exec -u 0 -it centos /bin/bash
[root@centos /]# cd /app
[root@centos app] javac Main.java
[root@centos app] java Main
2020-02-20T21:53:36.132
2020-02-20T21:53:37.113
2020-02-20T21:53:38.113
^C
[root@centos app] LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1 FAKETIME="-15d" FAKETIME_DONT_FAKE_MONOTONIC=1 java Main
2020-02-05T21:55:51.787
2020-02-05T21:55:52.766
2020-02-05T21:55:53.765
^C

It's working as expected, though I added another variable to the command: FAKETIME_DONT_FAKE_MONOTONIC=1. From the README of libfaketime:

Java-/JVM-based applications work but you need to pass in an extra argument
(FAKETIME_DONT_FAKE_MONOTONIC). See usage basics below for details. Without
this argument the java command usually hangs.

Indeed, without it even this tiny program hanged after printing the first date. I must admit I wasted some time debugging it just because I skipped reading the readme.

Cleaning it up

To shorten the commands, LD_PRELOAD and FAKETIME_DONT_FAKE_MONOTONIC values can be specified as the environment values of the Docker image. I omitted FAKETIME because this one is likely to change.

...

USER root

ENV LD_PRELOAD /usr/local/lib/faketime/libfaketime.so.1
ENV FAKETIME_DONT_FAKE_MONOTONIC 1

RUN ...

After these changes, it should be possible to run FAKETIME="-15d" java Main to render the same output as before.

Changing the time dynamically at runtime

It is possible to specify the FAKETIME value in a file instead of a variable. It makes it possible to change the value at any moment. libfaketime will pick it up after ten seconds7.

It is possible to do this system-wide or just for a user. I'll describe the latter.

The file needs to be named .faketimerc and it needs to be placed in the home directory. It should contain only the value of the FAKETIME variable, just like this:

-15d

Running java Main in the shell should now render the expected output. While the program running keeps running, in another shell session we can type echo -10d > ~/.faketimerc. The output should change after ten seconds.

Different variants of the FAKETIME value

Specifying a relative offset is not the only way to fake the time. Here are some more variants:

  • different offset mutlipliers: all the examples used "-16d", but instead of "d" it can also be "m", "h", "y" or nothing for seconds; the offset can be set in the past (-10d) or in the future (+10d) > EXAMPLES: > * -120 is 120 seconds behind > * +2h is 2 hours in the future > * +1y is 1 year in the future
  • 'start at' date: FAKETIME="@2020-12-24 20:30:00", where the clock will start ticking from this date for each new process, but it's possible to configure it to keep the clock ticking instead7
  • absolute date: FAKETIME="2020-12-24 20:30:00" will render a fixed value, as if the time stopped at this point

Summary

These are not all the features that libfaketime supports. I suggest skimming through the list of features in the readme just to get familiar with the possiblities - just in case you ever need to use any of them.

Key takeways

  • I'll use libfaketime in cases similar to this one
  • LD_PRELOAD mechanism can be used on Linux machines to replace pieces of code without modifying the original code
  • I should have read the friendly manual earlier to save time

Further reading

Sources


  1. https://forums.docker.com/t/is-it-possible-to-change-time-dynamically-in-docker-container/56787/5  

  2. https://forums.docker.com/t/change-containers-year/58880/3 

  3. https://stackoverflow.com/a/29561602 

  4. http://www.goldsborough.me/c/low-level/kernel/2016/08/29/16-48-53-the_-ld_preload-_trick/ 

  5. https://blog.fpmurphy.com/2012/09/all-about-ld_preload.html 

  6. https://rafalcieslak.wordpress.com/2013/04/02/dynamic-linker-tricks-using-ld_preload-to-cheat-inject-features-and-investigate-programs/ 

  7. https://github.com/wolfcw/libfaketime 

Top comments (0)