Testing the safe time64 transition path

Recently I’ve been elaborating on the perils of transition to 64-bit time_t, following the debate within Gentoo. Within these deliberations, I have also envisioned potential solutions to ensure that production systems could be migrated safely.

My initial ideas involved treating time64 as a completely new ABI, with a new libdir and forced incompatibility between binaries. This ambitious plan faced two disadvantages. Firstly, it required major modification to various toolchains, and secondly, it raised compatibility concerns between Gentoo (and other distributions that followed this plan) and distributions that switched before or were going to switch without making similar changes. Effectively, it would not only require a lot of effort from us, but also a lot of convincing other people, many of whom probably don’t want to spend any more time on doing extra work for 32-bit architectures. This made me consider alternative ideas.

One of them was to limit the changes to the transition period — use a libt32 temporary library directory to prevent existing programs from breaking while rebuilds were performed, and then simply remove them, and be left with plain lib like other distributions that switched already. In this post, I’d like to elaborate how I went about testing the feasibility of this solution. Please note that this is not a migration guide — it includes steps that are meant to detect problems with the approach, and are not suitable for production systems.

Preparing to catch time32/time64 mixing

As I’ve explained before, the biggest risk during the transition is accidental mixing of time32 and time64 binaries. In the worst case, it could mean not only breaking programs running on production, but actively creating vulnerabilities via out-of-bounds accesses. Therefore, I believe it is crucial to ensure that no such thing happens throughout the migration.

My first step towards testing the migration process was to create an ABI mixing check that would be injected into executables. I’ve placed the following code into /usr/include/__gentoo_time.h:

#include <stdio.h>
#include <stdlib.h>

__attribute__((weak))
__attribute__((visibility("default")))
struct {
	int time32;
	int time64;
} __gentoo_time_bits;

__attribute__((constructor))
static void __gentoo_time_check() {
#if _TIME_BITS == 64
#error "not now"
	__gentoo_time_bits.time64 = 1;
#else
	__gentoo_time_bits.time32 = 1;
#endif

	if (__gentoo_time_bits.time32 && __gentoo_time_bits.time64) {
		FILE *f;
		fprintf(stderr, "time32 and time64 ABI mixing detected\n");
		/* trigger a sandbox failure for good measure too */
		f = fopen("/time32-time64-mixing", "w");
		if (f)
			fclose(f);
		abort();
	}
}

Then, I have added the following line to /usr/include/time.h, just above __BEGIN_DECLS:

#include <__gentoo_time.h>

Now, this meant that any binary including <time.h>, even indirectly, would get our check. In fact, the check would probably be duplicated a lot, but that’s not really a problem for the test system.

The check itself utilizes a bit of magic. It creates a weak __gentoo_time_bits structure that would be shared between the executable itself and all loaded libraries. Every binary would run the constructor function upon loading, and it would fits store its own _TIME_BITS value within the shared structure, and then ensure that no binary set the other value. If that did happen, it would not only cause the program to immediately abort, but also try to trigger a sandbox failure, so the package build would be considered failed even if the build system ignored that particular failure.

However, note the #error in the snippet. This is a temporary hack to block packages that automatically try to use -D_TIME_BITS=64 (e.g. coreutils, grep, man-db), as they would trigger the check prematurely, and as a false positive.

At this point, I did rebuild the whole system, except for glibc, to inject the check into as many time32 binaries as possible:

emerge -ve --exclude=sys-libs/glibc --keep-going=y --jobs=16 @world

A number of packages fail here, because they attempt to force -D_TIME_BITS=64. This is okay, we don’t need perfect coverage, and we definitely don’t want false positives.

Preparing for the transition

The next step is to actually prepare for the transition. The preparation involves two changes, to all packages except for sys-libs/glibc:

  1. Moving all libraries from lib to libt32.
  2. Injecting libt32 directories into RUNTIME of all binaries, executables and libraries alike.

This is done using a tool called time32-prep. It takes care of finding all potential libdirs from ld.so, setting RUNPATH on binaries (and removing any references to plain lib, while at it), and then moving the libraries.

Rebuilding everything

The next step is to configure the system to compile time64 binaries by default. For a start, I have added the following snippet to make.conf, to easily distinguish packages that were rebuilt:

CHOST="i686-pc-linux-gnut64"
CHOST_x86="i686-pc-linux-gnut64"

I’ve rebuilt the dependencies of GCC using time64 flags explicitly:

CFLAGS="-D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64" emerge -1v sys-apps/sandbox dev-libs/{gmp,mpfr,mpc} sys-libs/zlib app-arch/{xz-utils,zstd}

Rebuilt and switched binutils:

emerge -1v sys-devel/binutils
binutils-config 1

Then, I’ve added a user patch to make GCC default to time64:

--- a/gcc/c-family/c-cppbuiltin.cc
+++ b/gcc/c-family/c-cppbuiltin.cc
@@ -1560,6 +1560,9 @@ c_cpp_builtins (cpp_reader *pfile)
     builtin_define_with_int_value ("_FORTIFY_SOURCE", GENTOO_FORTIFY_SOURCE_LEVEL);
 #endif
 
+  cpp_define (pfile, "_FILE_OFFSET_BITS=64");
+  cpp_define (pfile, "_TIME_BITS=64");
+
   /* Misc.  */
   if (flag_gnu89_inline)
     cpp_define (pfile, "__GNUC_GNU_INLINE__");

And rebuilt GCC itself (without time64 flags):

USE=-sanitize emerge -v sys-devel/gcc
gcc-config 1

Note that I had to disable sanitizers, as they currently fail to build with _TIME_BITS=64. I also had to comment out the __gentoo_time.h include for the time of building GCC.

The final step was to rebuild all packages (except for GCC and glibc) with the new compiler:

emerge -ve --exclude=sys-libs/glibc --exclude=sys-devel/{binutils,gcc} --jobs=16 --keep-going=y @world

The results

Well, I have some bad news — at some point, the rebuilds started failing. However, it seems that all failures I’ve hit during the initial testing can be accounted for as something relatively harmless — Perl and Python extensions.

Long story short, since they are installed into a dedicated directory, they can’t be prevented from ABI mixing via the libt32 hack. However, that’s unlikely to be a real problem. They failed for me, because I’ve made ABI mixing absolutely fatal — but in reality only private parts of the Python API use time_t, and these should not be used by any third-party extensions. And in the end, the issues are resolved by rebuilding in a different order.

Next steps

While this could be considered an important success, we’re still way ahead from being ready to go full time64. The time32-prep tool itself has a few TODOs, and definitely needs testing on a more “production-like” system. Then, there are actual problems that the packages are facing on time64 setups (like the GCC build failure in sanitizers), and that need to be fixed before we make things official.

Leave a Reply

Your email address will not be published.