log.pocka.io

C timezone functions inside Nix access incorrect zoneinfo directory

Created at
Updated at

C standard library loads timezone information by accessing /usr/share/zoneinfo (timezone database directory). In Nix, due to its sandbox nature, GNU libc (one of standard library implementation, glibc package in Nix) tries to read that directory under its own Nix store. For example, compiling and running this program inside Nix,

// main.zig
const libc = @cImport({
    @cInclude("time.h");
});

pub fn main() void {
    libc.tzset();

    std.debug.print("{d}\n", .{libc.timezone});
}
TZ=Europe/Paris ./zig-out/bin/main

always print 0 (UTC as a fallback).

A binary built in normal way accesses /usr/share/zoneinfo,

TZ=Europe/Paris strace ./zig-out/bin/main 2>&1 | grep Paris

# Output:
# openat(AT_FDCWD, "/usr/share/zoneinfo/Europe/Paris", O_RDONLY|O_CLOEXEC) = 3

meanwhile binary built inside Nix accesses glibc package's store path:

TZ=Europe/Paris strace ./zig-out/bin/main 2>&1 | grep Paris

# Output:
# openat(AT_FDCWD, "/nix/store/cg9s562sa33k78m63njfn1rw47dp9z0i-glibc-2.40-66/share/zoneinfo/Europe/Paris", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

This won't be a problem on a system where $TZDIR environment variable set. The lookup function (tzset and functions that internally call tzset) accesses the path in the environment variable rather than glibc package's store.

Workaround

Although there is a bug report for this, no action has been taken because NixOS sets $TZDIR thus not a problem for them, and non-NixOS issue seems to be very low priority.

As far as I know, there is no option to specify a path for zoneinfo directory for Nix (I did not searched for glibc compile time options because that would lead to affecting other programs' behavor or duplicating glibc package.)

Because of that, you have to "patch" in runtime—by letting a user set $TZDIR or your program setting the $TZDIR if it's unset. The former is obvious: having a section in documents and call it a day.

The latter is still easy, but not clean.

Write code to set $TZDIR

If you have control over application code (and not hesitant about adding Nix specific code,) expose compile-time option and set the value to $TZDIR whenever the environment variable is unset.

// main.zig

pub fn main() void {
    // [snip]

    // Assuming `config` is compile-time options and has `tzdir: ?[]const u8`
    const config = @import("config");

    const existing = stdlib.getenv("TZDIR");
    if (existing) |tzdir| {
        if (std.mem.span(tzdir).len > 0) {
            return;
        }
    }

    const fallback = config.tzdir orelse return;
    const set_result = stdlib.setenv("TZDIR", fallback, 1);
    if (set_result != 0) {
        std.log.warn("Failed to set $TZDIR to {s}: {d}", .{
            fallback,
            set_result,
        });
    }

    // [snip]
}

You can set timezone data directory from Nix package for reproducibility or just use system's one.

Wrapper script

If you're creating a package for an existing binary, wrapper script would be the easiest choice.

# my-app.nix
pkgs.writeShellApplication {
  name = "my-app";
  text = ''
    TZDIR=${tzdata}/share/zoneinfo ${pkgs.my-app-bin}/bin/my-app "$@"
  '';
}

Sample project

You can test and inspect these behavor with this sandbox repository.