Janvier 2038 pour Linux 32 bits

Christophe BLAESS - février 2022

Vous avez sûrement déjà entendu parler du fameux «bug de 2038» qui menace certains systèmes informatiques. Qu'en est-il exactement et quels sont les risques encourus par les machines que nous déployons aujourd'hui ?

Le 19 janvier dernier, j'ai vu passer un tweet d'Olivier Poncet (@ponceto91) qui faisait référence à ce problème dans son thread quotidien #LaPetiteInfoDuJour. Comme c'est un sujet que j'aborde régulièrement lors de mes sessions de formation sur les systèmes embarqués, j'ai eu envie de le développer en détail ici.

TLDR : le problème concerne la plupart des systèmes embarqués Linux 32 bits conçus aujourd'hui, mais pas les systèmes 64 bits. Qu'on prenne une distribution toute faite comme Raspberry Pi OS ou qu'on génère un système avec Buildroot ou Yocto Project et leurs options par défaut, le risque est présent. Avec une bonne configuration de la libC on peut régler le problème, mais ce n'est pas trivial et il est important de s'y prendre au plus tôt.

Problématique

Sur les systèmes Unix et leurs dérivés comme Linux, on a coutume de mesurer l'heure à partir d'une référence commune : le 1er janvier 1970 à 0h. C'est ce que l'on considère comme le début de l'Ère (Epoch) Unix.

Cette heure est mesurée en secondes et stockée dans un type time_t utilisé par l'appel système time(). Ce type est également employé dans des structures comme timespec renseignée par l'appel système clock_gettime() où l'heure est assortie d'un complément en nanosecondes.

Le 19 janvier 2038 à 03:14:07 UTC le nombre de secondes aura atteint la dernière valeur représentable sur 31 bits : 0x7FFF FFFF en hexadécimal soit environ 2.15 milliards. À 03:14:08 ce nombre de secondes atteindra 231 soit 0x8000 0000.

Et alors ? Sur les systèmes 32 bits, le type time_t étant signé, le nombre de secondes basculera à -2.15 milliards, et l'heure représentée correspondra alors au 13 décembre 1901 à 20:45:52 UTC. Sur les systèmes 64 bits, l'heure continuera à s'écouler tout à fait normalement.

Répétons-le clairement : le problème de l'année 2038 ne concerne que les systèmes 32 bits, il n'y aura pas de souci pour les systèmes 64 bits.. Mais même si le nombre de processeurs 32 bits va diminuant, il est encore conséquent dans le monde de l'embarqué (notamment les architectures ARM) où des impératifs de pérennité, principalement dans le domaine industriel, sont souvent présents.

Pour être tout à fait exact, il y aura bien un problème sur les systèmes 64 bits mais pas en 2038, dans 292 milliards d'années ! Toutefois la Terre disparaîtra dans 5 milliards d'années environ...

On peut se demander pour quelle raison le type time_t est signé puisqu'il contient un nombre de secondes, ce qui ne peut logiquement être négatif ? Cela vient de l'appel système time() l'un des plus anciens sous Unix. Son prototype est le suivant :

  time_t time(time_t *);

Ceci signifie qu'on peut l'appeler pour récupérer le nombre de secondes écoulées depuis le 01/01/1970 dans une variable time_t de notre code :

  time_t  now;

  time(&now);

On peut aussi l'appeler avec un argument NULL pour récupérer directement l'heure :

   now = time(NULL);

Le problème, c'est qu'il est également possible de lui passer (par erreur) un pointeur invalide. Pas un pointeur NULL, pour lequel son comportement est explicitement prévu, mais un pointeur invalide, mal initialisé ou déjà libéré par exemple :

  time_t *now;

  now = malloc(sizeof(time_t));
  // ...
  free(now);
  time(now);

Dans ce cas, time() doit se comporter comme tous les appels système Unix, et renvoyer la valeur -1 (en remplissant la variable errno avec le code d'erreur EFAULT). Pour renvoyer la valeur -1, il faut donc que son type de retour (time_t) soit signé. CQFD !

Bien sûr il «suffit» de définir un type time_t sur 64 bits pour régler le problème. Mais ce n'est pas si simple car pour cela il faut que tout l'ensemble du système (noyau, bibliothèques, systèmes de fichiers, utilitaires, applicatif métier, etc.) soit d'accord sur la taille de ce type de donnée. Et nous allons voir que cela n'est pas si facile qu'on le pense au premier abord.

Je vais me limiter dans les exemples à venir à l'utilisation de l'appel time() et parler du type time_t, mais ce dernier est employé pour définir d'autres structures de données (timeval, timespec...) et notre problématique concerne bien d'autres appels système (gettimeofday(), clock_gettime(), timer_create(), etc.)

Effets observables

Le type time_t sur 32 bits ne pourra donc pas survivre à la nuit du 18 au 19 janvier 2038. Comment observer le problème ? Simplement en avançant la date système.

La commande «date» que l'on peut appeler depuis le shell joue plusieurs rôles. Tout d'abord, on s'en doute, elle affiche la date courante sur la sortie standard. Mais elle permet également de configurer la nouvelle date système. Pour cela il faut lui fournir en argument, une chaîne de 12 chiffres 

    <Mois><Jour><Heure><Minute><Année>

Par exemple 022109002022 pour «21 février 2022 à 09:00» ou 011903142038 pour «19 janvier 2038 à 03:14». On peut également ajouter les secondes après un point décimal, mais ça ne nous servira pas ici.

Nous devons de préférence nous placer sur un système déconnecté du réseau (sinon le service client NTP risque de rétablir l'heure avant que nous n'ayons le temps de voir quoi que ce soit). Il va sans dire qu'il ne faut pas mener cette expérience sur un système utilisé en production, il peut y avoir des effets indésirables notamment lors du retour en arrière de l'heure à la fin du test.

Distribution Raspberry Pi OS 32-bits

Notre premier essai va être réalisé sur un Raspberry Pi 4 avec la distribution officielle «Raspberry Pi OS 2022/01/28», basée sur la Debian 11 «Bullseye», en version «lite 32-bits».

Pour faire facilement des captures d'écran, je me connecte sur son port série après avoir ajouté la ligne «enable_uart=1» dans le fichier config.txt de la première partition (BOOT). J'ai également modifié le prompt (variable d'environnement PS1) afin d'identifier les traces de cette image par rapport aux autres que nous verrons plus bas.

Raspbian GNU/Linux 11 raspberrypi ttyS0

raspberrypi login: pi
Password: (raspberry)
Linux raspberrypi 5.10.92-v7l+ #1514 SMP Mon Jan 17 17:38:03 GMT 2022 armv7l
[...]

[RPi-OS 32]$ date
Tue 15 Feb 07:25:04 GMT 2022

[RPi-OS 32]$ date 011903142038
date: cannot set date: Operation not permitted

Pour configurer la date, il faut évidemment avoir les permissions root.

[RPi-OS 32]$ sudo date 011903142038
Tue 19 Jan 03:14:00 GMT 2038

Message from syslogd@raspberrypi at Jan 19 03:14:00 ...
 systemd[1]: Failed to run main loop: Invalid argument

Broadcast message from systemd-journald@raspberrypi (Tue 2038-01-19 03:14:00 GMT):

systemd[1]: Failed to run main loop: Invalid argument


Message from syslogd@raspberrypi at Jan 19 03:14:00 ...
 systemd[1]: Freezing execution.

Broadcast message from systemd-journald@raspberrypi (Tue 2038-01-19 03:14:00 GMT):

systemd[1]: Freezing execution.

Nous avons déjà une rafale de messages provenant de systemd toutefois cela n'a aucun rapport avec notre sujet, c'est le saut brutal vers l'avant qui lui déplait.

Appelons la commande date en consultation de l'heure, en la répétant pendant quelques secondes jusqu'à atteindre 03:14:08.

[RPi-OS 32]$ date
Tue 19 Jan 03:14:01 GMT 2038

[RPi-OS 32]$ date
Tue 19 Jan 03:14:02 GMT 2038

[RPi-OS 32]$ date
Tue 19 Jan 03:14:03 GMT 2038

[RPi-OS 32]$ date
Tue 19 Jan 03:14:04 GMT 2038

[RPi-OS 32]$ date
Tue 19 Jan 03:14:05 GMT 2038

[RPi-OS 32]$ date
Tue 19 Jan 03:14:06 GMT 2038

[RPi-OS 32]$ date
Tue 19 Jan 03:14:07 GMT 2038

[RPi-OS 32]$ date
[  127.774688] systemd-journald[137]: Assertion 'clock_gettime(map_clock_id(clock_id), &ts) == 0' fa
iled at src/basic/time-util.c:54, function now(). Aborting.

Thu  1 Jan 01:00:00 BST 1970

[RPi-OS 32]$ date
Thu  1 Jan 01:00:00 BST 1970

[RPi-OS 32]$ date
Thu  1 Jan 01:00:00 BST 1970

Nous apercevons une erreur de systemd qui, à présent, semble liée à notre sujet. Mais surtout nous voyons que l'horloge du système est revenue à l'heure zéro et ne progresse plus !

Même si la consultation de l'heure échoue, nous pouvons passer des commandes normalement sur le système :

[RPi-OS 32]$ ls /proc/
1    19   27   348  425  63   8   98           iomem          sched_debug
10   2    276  349  427  632  80  99           ioports        schedstat
100  20   278  35   433  64   81  asound       irq            self
101  203  28   353  484  648  82  buddyinfo    kallsyms       slabinfo
  [...]
18   254  342  412  604  76   95  filesystems  net            zoneinfo
185  255  345  415  61   77   96  fs           pagetypeinfo
186  26   347  424  62   79   97  interrupts   partitions

[RPi-OS 32]$ uname -a
Linux raspberrypi 5.10.92-v7l+ #1514 SMP Mon Jan 17 17:38:03 GMT 2022 armv7l GNU/Linux

Essayons de rétablir l'heure :

[RPi-OS 32]$ sudo date 021507292022
sudo: unable to get time of day: Value too large for defined data type
sudo: error initializing audit plugin sudoers_audit

[RPi-OS 32]$ 

C'est impossible, la commande sudo elle-même échoue en essayant de lire l'heure (pour les logs). Nous ne pouvons pas non plus redémarrer proprement le système.

[RPi-OS 32]$ sudo reboot
sudo: unable to get time of day: Value too large for defined data type
sudo: error initializing audit plugin sudoers_audit

[RPi-OS 32]$ 

Distribution Raspberry Pi OS 64-bits

Si nous faisons la même opération sur la version «lite 64-bits» de la distribution «Raspberry Pi OS», il n'y a aucun souci :

[RPi-OS 64]$ uname -a
Linux raspberrypi 5.10.92-v8+ #1514 SMP PREEMPT Mon Jan 17 17:39:38 GMT 2022 aarch64 GNU/Linux

[RPi-OS 64]$ lscpu
Architecture:                    aarch64
CPU op-mode(s):                  32-bit, 64-bit
  [...]

[RPi-OS 64]$ date
Tue 15 Feb 08:24:00 GMT 2022

[RPi-OS 64]$ sudo date 011903142038
Tue 19 Jan 03:14:00 GMT 2038

[RPi-OS 64]$ date
Tue 19 Jan 03:14:02 GMT 2038

[RPi-OS 64]$ date
Tue 19 Jan 03:14:05 GMT 2038

[RPi-OS 64]$ date
Tue 19 Jan 03:14:07 GMT 2038

[RPi-OS 64]$ date
Tue 19 Jan 03:14:08 GMT 2038

[RPi-OS 64]$ date
Tue 19 Jan 03:14:09 GMT 2038

[RPi-OS 64]$ date
Tue 19 Jan 03:14:10 GMT 2038

La version 64 bits de cette distribution ne fonctionne toutefois que sur les Raspberry Pi 3 et 4, pas sur les précédents.

Le thème de cet article étant précisément les systèmes 32 bits, continuons nos expériences avec d'autres installations possibles.

Même si la robustesse de Raspberry Pi OS s'est améliorée dans les dernières versions (bootloader évolué, possibilité de monter le système de fichiers en lecture seulement, etc.), il est déconseillé d'utiliser une distribution généraliste toute faite dans un système embarqué autonome. On préférera utiliser une image construire sur mesure avec des outils prévus pour l'embarqué, comme Buildroot ou encore Yocto Project.

Buildroot 2022

La version long term 2022 de Buildroot n'est pas encore sortie au moment de la rédaction de ces lignes, aussi mes essais sont-ils réalisés sur la version release candidate disponible. Je résume très brièvement mes actions. On se reportera à l'article «Créer un système complet avec Buildroot» pour en savoir plus.

$ wget https://buildroot.org/downloads/buildroot-2022.02-rc1.tar.gz
$ tar xf buildroot-2022.02-rc1.tar.gz
$ make O=../build-pi4-br  raspberrypi4_defconfig
$ cd ../build-pi4-br/
$ make
 [...]

Après avoir flashé le fichier «sdcard.img» obtenu sur une carte SD et démarré un Raspberry Pi 4, j'observe :

Welcome to Buildroot
buildroot login: root

[Buildroot]# date
Thu Jan  1 00:00:15 UTC 1970

[Buildroot]# date 011903142038
Tue Jan 19 03:14:00 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:01 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:03 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:05 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:07 UTC 2038

[Buildroot]# date
Fri Dec 13 20:45:52 UTC 1901

[Buildroot]# date
Fri Dec 13 20:45:54 UTC 1901

[Buildroot]# date
Fri Dec 13 20:45:56 UTC 1901

[Buildroot]# date
Fri Dec 13 20:45:58 UTC 1901

Nous ne sommes plus bloqués en 1970, mais nous voici revenus en 1901 ! Après avoir savouré un instant cet anachronisme, nous pouvons vérifier que le système fonctionne néanmoins correctement.

[Buildroot]# touch my-file

[Buildroot]# ls -l
total 0
-rw-r--r--    1 root     root             0 Dec 13 20:46 my-file

[Buildroot]# ls /proc/
1              25             7              cmdline        meminfo
10             26             70             consoles       misc
11             27             71             cpu            modules
111            29             72             cpuinfo        mounts
114            3              73             crypto         net
118            30             74             device-tree    pagetypeinfo
12             31             75             devices        partitions
13             33             76             diskstats      sched_debug
139            34             78             driver         schedstat
 [...]

[Buildroot]# uname -a
Linux buildroot 5.10.92-v7l #1 SMP Thu Feb 17 03:16:35 CET 2022 armv7l GNU/Linux

[Buildroot]# ls -l /lib/libc*
lrwxrwxrwx    1 root     root            19 Feb 17  2022 /lib/libc.so.0 -> libuClibc-1.0.40.so
lrwxrwxrwx    1 root     root            19 Feb 17  2022 /lib/libc.so.1 -> libuClibc-1.0.40.so

[Buildroot]# 

En regardant le contenu du répertoire /lib, nous pouvons remarquer que la bibliothèque C choisie par défaut par Buildroot est la uClibC-ng, nous en reparlerons.

Avec cette expérience nous voyons que le système ne crashe pas, ne se comporte pas de manière inattendue, mais qu'il affiche simplement une date invalide.

Pour certains systèmes embarqués cela peut convenir. Néanmoins dès que le système doit interagir avec l'environnement extérieur en incluant la date (horodatage d'événements, fichiers de logs, etc.), en comparant des instants (durée de stationnement dans un parking, délai pour arréter un actionneur, etc.) ou simplement en affichant l'heure sur un terminal, ce n'est pas satisfaisant.

Yocto Project Honister

Nous reviendrons à Buildroot un petit peu plus bas, faisons une tentative avec Yocto Project. Je prends la branche Honister qui date de novembre 2021. Je rajouterai une note si le comportement par défaut change avec les branches à venir.

Comme pour Buildroot, je ne détaille pas les commandes utilisées pour la construction de l'image avec Yocto, je vous renvoie à l'article «Linux embarqué avec Yocto Project» pour en savoir plus.

$ git clone git://git.yoctoproject.org/poky -b honister
$ git clone git://git.yoctoproject.org/meta-raspberrypi -b honister
$ source poky/oe-init-build-env build-pi4-yocto
$ bitbake-layers add-layer ../meta-raspberrypi
$ nano conf/local.conf
  MACHINE = "raspberrypi4"
  ENABLE_UART = "1"
$ bitbake core-image-base

Le fichier «tmp/deploy/images/raspberrypi4/core-image-base-raspberrypi4.wic.bz2» est alors décompressé puis flashé sur une carte SD.

Poky (Yocto Project Reference Distro) 3.4.2 raspberrypi4 /dev/ttyS0

raspberrypi4 login: root
[Yocto]# date
Fri Mar  9 12:35:38 UTC 2018

[Yocto]# date 011903142038
Tue Jan 19 03:14:00 UTC 2038

[Yocto]# date
Tue Jan 19 03:14:04 UTC 2038

[Yocto]# date
Tue Jan 19 03:14:06 UTC 2038

[Yocto]# date
Tue Jan 19 03:14:07 UTC 2038

[Yocto]# date
Thu Jan  1 00:00:00 UTC 1970

[Yocto]# date
Thu Jan  1 00:00:00 UTC 1970

[Yocto]# ls

[Yocto]# touch my-file

[Yocto]# ls -l
ls: .: Value too large for defined data type

[Yocto]# uname -a
Linux raspberrypi4 5.10.78-v7l #1 SMP Mon Nov 8 18:19:44 UTC 2021 armv7l GNU/Linux

[Yocto]# ls /proc/
ls: /proc/fb: Value too large for defined data type
ls: /proc/fs: Value too large for defined data type
ls: /proc/bus: Value too large for defined data type
ls: /proc/irq: Value too large for defined data type
ls: /proc/tty: Value too large for defined data type
ls: /proc/keys: Value too large for defined data type
ls: /proc/kmsg: Value too large for defined data type
ls: /proc/misc: Value too large for defined data type
ls: /proc/stat: Value too large for defined data type
ls: /proc/iomem: Value too large for defined data type
ls: /proc/locks: Value too large for defined data type
ls: /proc/swaps: Value too large for defined data type
[...]

[Yocto]# ls /
ls: /: Value too large for defined data type

Nous voici de nouveau bloqués à l'aube des années 1970, mais cette fois le système devient inutilisable. Certains appels système échouent. Ce n'est pas acceptable pour un système embarqué autonome.

Premières réflexions

Les systèmes que nous avons testés ne sont donc pas capables de franchir correctement le mois de janvier 2038. Toutefois, nous allons pouvoir améliorer la situation, à condition de bien comprendre d'où vient le problème.

La page de manuel time(2) nous annonce laconiquement «Applications intended to run after 2038 should use ABIs with time_t wider than 32 bits.».

Ceci ne nous aide pas beaucoup, nous avions bien compris que le problème vient de la taille des time_t mais comment la modifier ?

Le premier réflexe lorsqu'un problème se pose avec un système Linux est souvent de mettre en cause le kernel.

Les versions de noyaux utilisées dans les systèmes ci-dessus sont récentes :

Le problème ne vient pas du noyau 5.10. En effet, depuis sa version 3.4 (mai 2012), le kernel définit sur les systèmes 32 bits, le type time_t comme un long long (64 bits) alors qu'il s'agissait d'un int auparavant. La disponibilité de tous les appels système liés au temps en version 64 bits est plus récentes, mais la branche 5.10 est totalement prête à passer 2038.

Pas de souci avec le noyau, donc. Mais alors d'où vient le problème ?

N'oublions pas que les applications de l'espace utilisateur n'interagissent pas directement avec le kernel. La libC (bibliothèque C) sert d'intermédiaire entre les processus et le noyau, même pour les applications écrites dans un autre langage que le C.

Pour connaître les versions utilisées, il suffit généralement de rechercher un fichier libc.so.* sous /lib/ et de regarder sa cible s'il s'agit d'un lien. En outre avec la Gnu LibC ou la Musl, le fichier est exécutable et affiche son numéro de version.

[RPi-OS 32]$ /lib/arm-linux-gnueabihf/libc.so.6
GNU C Library (Debian GLIBC 2.31-13+rpt2+rpi1+deb11u2) stable release version 2.31.
Copyright (C) 2020 Free Software Foundation, Inc.
[...]
[RPi-OS 32]$ 
[Buildroot]# ls -l /lib/libc*
lrwxrwxrwx    1 root     root            19 Feb 17  2022 /lib/libc.so.0 -> libuClibc-1.0.40.so
lrwxrwxrwx    1 root     root            19 Feb 17  2022 /lib/libc.so.1 -> libuClibc-1.0.40.so
[Yocto]# /lib/libc.so.6
GNU C Library (GNU libc) stable release version 2.34.
Copyright (C) 2021 Free Software Foundation, Inc.
 [...]

Les versions rencontrées sont donc les suivantes :

La bibliothèque Gnu LibC supporte un time_t sur 64-bits depuis sa version 2.34. Il est donc normal que Raspberry Pi OS ne puisse pas franchir correctement 2038.

L'image produite par Yocto Project ne fonctionne pas mieux que les autres. C'est un peu surprenant car la Gnu libC y est suffisamment récente.

Le choix — assez discutable — fait par le projet Gnu LibC est de conserver un time_t à 32 bits sauf si on réclame explicitement une taille de 64 bits à la compilation en définissant une constante symbolique spécifique : _TIME_BITS=64.

Voyons la définition dans les sources de la Gnu libC :

// time/bits/type/time_t.h:
  [...]
/* Returned by `time'.  */
#ifdef __USE_TIME_BITS64
typedef __time64_t time_t;
#else
typedef __time_t time_t;
#endif
  [...]

La constante __USE_TIME_BITS64 est définie ici :

// sysdeps/unix/sysv/linux/features-time64.h:
  [...]
# if _TIME_BITS == 64
#  if ! defined (_FILE_OFFSET_BITS) || _FILE_OFFSET_BITS != 64
#   error "_TIME_BITS=64 is allowed only with _FILE_OFFSET_BITS=64"
#  elif __TIMESIZE == 32
#   define __USE_TIME_BITS64	1
#  endif
# elif _TIME_BITS == 32
#  if __TIMESIZE > 32
#   error "_TIME_BITS=32 is not compatible with __TIMESIZE > 32"
#  endif
# else
#  error Invalid _TIME_BITS value (can only be 32 or 64-bit)
# endif	

Nous voyons que pour ajouter à la confusion, il faut également qu'une seconde constante soit définie pour que la première soit acceptée : _FILE_OFFSET_BITS=64.

Or ces constantes ne sont visiblement pas définies par Yocto Project pour le moment. Peut-être dans l'avenir, mais en attendant les images que nous produisons aujourd'hui ne sont pas vraiment pérennes.

Dans le cas de Buildroot, nous avons vu qu'il utilisait la bibliothèque uClibC-ng or celle-ci définit toujours le time_t comme un long int soit 32 bits dans notre cas. On peut vérifier dans les sources de la uClibC-ng :

//  include/time.h:
  [...]
/* Returned by `time'.  */
typedef __time_t time_t;
  [...]
//  libc/sysdeps/linux/common/bits/types.h:
  [...]
__STD_TYPE __TIME_T_TYPE __time_t;	/* Seconds since the Epoch.  */
  [...]
//  libc/sysdeps/linux/common/bits/typesizes.h`:
  [...]
#define __TIME_T_TYPE		__SLONGWORD_TYPE
  [...]
//  libc/sysdeps/linux/common/bits/types.h:
  [...]
#define __SLONGWORD_TYPE	long int
  [...]

Buildroot 2022 et la GlibC

Buildroot permet de choisir le type de bibliothèque C à employer (option «C library» du menu «Toolchain» de la commande «make menuconfig»).

Après avoir choisi glibc et relancé une compilation complète avec «make clean; make» nous pouvons vérifier le comportement :

Welcome to Buildroot
buildroot login: root

[Buildroot]# /lib/libc.so.6
GNU C Library (Buildroot) stable release version 2.34.
 [...]

[Buildroot]# date
Thu Jan  1 00:00:13 UTC 1970

[Buildroot]# date 011903142038
Tue Jan 19 03:14:00 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:04 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:07 UTC 2038

[Buildroot]# date
Thu Jan  1 00:00:00 UTC 1970

[Buildroot]# date
Thu Jan  1 00:00:00 UTC 1970

[Buildroot]# touch my-file

[Buildroot]# ls
ls: .: Value too large for defined data type

[Buildroot]# 

Comme avec Yocto Project, la version de la Gnu libC est 2.34, mais le type time_t est sur 32 bits par défaut.

Nous pouvons essayer de compiler un petit programme qui affiche la taille des types qui nous intéressent, et qui appelle time() pour lire l'heure.

// get-sizes-and-date.c

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

int main(void)
{
	time_t now;
	char line[1024];
	struct tm *tm_now;

	printf("sizeof(int) = %lu\n", sizeof(int));
	printf("sizeof(long int) = %lu\n", sizeof(long int));
	printf("sizeof(long long int) = %lu\n", sizeof(long long int));
	printf("sizeof(time_t) = %lu\n", sizeof(time_t));

	time(&now);
	tm_now = gmtime(&now);
	strftime(line, 1023, "%x %X", tm_now);
	printf("%s\n", line);

	return 0;
}

Buildroot nous fournit un SDK complet (chaîne de cross-compilation et fichiers headers des bibliothèques incluses durant la création du système), mais par souci de simplicité je vais simplement utiliser le cross-compiler produit durant le build.

Je fais deux compilations, la première sans les constantes symboliques, et la seconde avec les constantes.

$ ./host/bin/arm-buildroot-linux-gnueabihf-gcc  -o get-sizes-and-date-1 ./get-sizes-and-date.c 

$ ./host/bin/arm-buildroot-linux-gnueabihf-gcc  -o get-sizes-and-date-2 ./get-sizes-and-date.c -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 

Puis je transfère ces deux exécutables dans le répertoire /root de mon image. Après avoir redémarré le Raspberry Pi et reconfiguré la date, j'observe :

[Buildroot]# date
Thu Jan  1 00:00:00 UTC 1970

[Buildroot]# ./get-sizes-and-date-1
sizeof(int) = 4
sizeof(long int) = 4
sizeof(long long int) = 8
sizeof(time_t) = 4
01/01/70 00:00:00

Nous voyons que le premier exécutable (sans les constantes symboliques) ne peut pas lire l'heure correctement, ce qui s'explique par sizeof(time_t) valant 32 bits.

[Buildroot]# ./get-sizes-and-date-2
sizeof(int) = 4
sizeof(long int) = 4
sizeof(long long int) = 8
sizeof(time_t) = 8
01/19/38 03:14:15

En revanche le second exécutable (compilés avec les constantes symboliques) fonctionne normalement, et peut lire la date puisque pour lui le type time_t mesure 64 bits.

Notons que le système lui-même fonctionne très moyennement puisque la commande «ls» (fournie par Busybox) n'arrive pas à lire les timestamps des fichiers et échoue :

[Buildroot]# ls
ls: ./get-sizes-and-date-1: Value too large for defined data type
ls: ./get-sizes-and-date-2: Value too large for defined data type

[Buildroot]# 

Au final nous avons un système qui fonctionne de manière très incertaine et suivant les options de compilation notre code applicatif marche plus ou moins bien.

La solution consisterait probablement à compiler tout le système avec les constantes symboliques définies mais ça ne me semble pas possible avec les versions actuelles de Buildroot.

Buildroot 2022 et MUSL

Il existe une autre bibliothèque C utilisable avec Buildroot et Yocto : la bibliothèque Musl.

Écrite par Rich Felker (@richfelker) spécifiquement pour les environnements embarqués, elle présente de nombreux avantages sur ses concurrentes en terme de taille de code, vitesse d'exécution, et conformités aux standards. On peut consulter ce site pour voir des comparaisons entre les différentes libC.

Pour compiler notre image Buildroot avec Musl, il faut procéder comme pour la sélection de la GlibC précédemment :

$ make menuconfig
  --> Toolchain
    --> C library
      --> musl
$ make clean; make
Welcome to Buildroot
buildroot login: root

[Buildroot]# date
Thu Jan  1 00:00:16 UTC 1970

[Buildroot]# date 011903142038
Tue Jan 19 03:14:00 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:04 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:07 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:08 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:09 UTC 2038

[Buildroot]# date
Tue Jan 19 03:14:10 UTC 2038

[Buildroot]# ls /
bin         lib         lost+found  opt         run         tmp
dev         lib32       media       proc        sbin        usr
etc         linuxrc     mnt         root        sys         var

[Buildroot]# ls /lib/libc.so -l
-rwxr-xr-x    1 root     root        632392 Feb 17  2022 /lib/libc.so

[Buildroot]# /lib/libc.so
musl libc (armhf)
Version 1.2.2
Dynamic Program Loader
Usage: /lib/libc.so [options] [--] pathname [args]

Voilà qui est intéressant ! Le système basé sur cette bibliothèque C passe allégrement la nuit du 19 janvier 2038. Sans que nous ayons quoique ce soit d'autre à faire que de sélectionner la bibliothèque «musl» dans le menu de configuration de Buildroot.

Voyons ce qu'il en est de notre code applicatif. Pour cela, nous allons utiliser le cross-compiler produit par Buildroot sans ajouter d'options particulières sur la ligne de commande de gcc.

$ ./host/bin/arm-buildroot-linux-musleabihf-gcc -o get-sizes-and-date get-sizes-and-date.c

Après avoir installé l'éxécutable sur la partition du système et redémarré le Raspberry Pi 4, nous observons :

[Buildroot]# ./get-sizes-and-date
sizeof(int) = 4
sizeof(long int) = 4
sizeof(long long int) = 8
sizeof(time_t) = 8
01/19/38 03:14:16

[Buildroot]# touch my-file

[Buildroot]# ls -l
total 8
-rwxr-xr-x    1 root     root          7564 Feb 17  2022 get-sizes-and-date
-rw-r--r--    1 root     root             0 Jan 19 03:14 my-file

Voici qui est plus rassurant que nos expériences précédentes !

Yocto Project Honister et MUSL

Nous pouvons demander à Yocto de compiler une image basée sur la bibliothèque Musl aussi facilement qu'avec Buildroot.

Il nous suffit d'ajouter la ligne suivante dans le fichier «conf/local.conf» :

  TCLIBC = "musl"

J'ai ajouté cette ligne sous les deux que j'avais inscrites dans l'expérience précédente :

  MACHINE ?= "raspberrypi4"
  ENABLE_UART = "1"

Je relance «bitbake -f core-image-base» et obtenu le comportement suivant, semblable à celui observé avec Buildroot :

Poky (Yocto Project Reference Distro) 3.4.2 raspberrypi4 /dev/ttyS0

raspberrypi4 login: root

[Yocto (musl)]# date
Fri Mar  9 12:35:58 UTC 2018

[Yocto (musl)]# date 011903142038
Tue Jan 19 03:14:00 UTC 2038

[Yocto (musl)]# date
Tue Jan 19 03:14:03 UTC 2038

[Yocto (musl)]# date
Tue Jan 19 03:14:06 UTC 2038

[Yocto (musl)]# date
Tue Jan 19 03:14:08 UTC 2038

[Yocto (musl)]# date
Tue Jan 19 03:14:09 UTC 2038

[Yocto (musl)]# date
Tue Jan 19 03:14:10 UTC 2038

[Yocto (musl)]# touch my-file

[Yocto (musl)]# ls -l
-rw-r--r--    1 root     root             0 Jan 19 03:14 my-file

[Yocto (musl)]# 

Conclusion

Récapitulons ce que nous avons observé au cours de ce long article.

Quelles conclusions en tirer pour la mise au point d'un système embarqué que l'on veut le plus pérenne possible ? Bien sûr il est tout à fait possible de simplement ignorer le problème et dire «Tant pis ! Le système sera obsolète et le client devra le remplacer…» Quelque chose me dit que si vous êtes arrivés jusqu'ici dans la lecture de cet article ce n'est probablement pas le choix que vous ferez ;-)

Tout d'abord la solution la plus simple est de privilégier un système 64 bits. Naturellement ce n'est pas toujours possible, pour des raisons de coûts ou de prise en charge d'un parc déjà déployé.

La seconde option est d'utiliser la bibliothèque Musl, en la sélectionnant dans Buildroot ou Yocto comme nous l'avons vu. C'est une très bonne solution, mais l'emploi de Musl n'est peut-être pas toujours possible. Certaines applications peuvent dépendre d'extensions spécifiques de la GNU libC (bien que cela soit rare).

Si vous dépendez de la Gnu libC, compilez tout votre code applicatif avec constantes symboliques décrites plus haut. Mais attention, certains utilitaires système ou certaines applications ne seront peut-être pas compilés de la sorte et une incohérence risque d'apparaître dans la taille des données échangées si elles contiennent des horodatages par exemple.

Beaucoup de systèmes sont «lâchés dans la nature» aujourd'hui sans prendre la moindre mesure pour 2038. Bien sûr on peut compter sur la possibilité de faire une mise à jour ultérieure du système d'ici là. Cela nécessite d'avoir un outil d'Update Over The Air (comme Rauc, Mender ou Swupdate).

Attention, ce n'est pas sans risque : si des données (paramètres, échantillons, traces, etc) sont enregistrées avec des champs contenant des horodatages sur 32 bits et que suite à une mise à jour du système la taille des champs est censée occuper 64 bits, on court à la corruption des données.

Autrement dit, il sera possible de mettre à jour un système déjà déployé (à condition d'avoir un outil d'update complet installé), mais cela devra se faire avec beaucoup de prudence et de test. N'oubliez pas que les utilitaires de mise à jour eux-même vérifient probablement l'horodatage des fichiers d'archives…

Derniers mots : si vous souhaitez de l'assistance pour vous accompagner sur des projets embarqués, je propose des prestations d'ingénierie et des sessions de formation chez Logilin.

Ce document est placé sous licence Creative Common CC-by-nc. Vous pouvez copier son contenu et le réemployer à votre gré pour une utilisation non-commerciale. Vous devez en outre mentionner sa provenance.