1 Punkte von GN⁺ 2024-09-30 | 1 Kommentare | Auf WhatsApp teilen
  • Auf 32-Bit-glibc-Systemen können nach 2038 Abfragen der aktuellen Zeit oder stat()-Aufrufe fehlschlagen; Gentoo braucht daher einen sicheren Migrationspfad zu 64-Bit-time_t
  • time64 in glibc muss zusammen mit Large File Support (LFS) verwendet werden; in 32-Bit-Umgebungen existieren dadurch die bisherige ABI, die LFS-ABI und die LFS+time64-ABI nebeneinander
  • Wenn time_t in APIs, Strukturen oder Funktionsargumenten vorkommt, führt die Änderung der Typbreite zu einem ABI-Bruch; das Mischen von time32- und time64-Binaries kann zu Laufzeitfehlern und Sicherheitsrisiken führen
  • Als quellbasierte Distribution kann Gentoo während eines Rebuilds von @world wegen Fehlern oder zirkulären Abhängigkeiten mit einem halb umgestellten System zurückbleiben
  • Nach der Korrektur vom 2024-09-30 reicht eine reine libdir-Umstellung nicht mehr aus; eine time64-Kennzeichnung, die auch den dynamischen Loader und Toolchains mehrerer Sprachen umfasst, bleibt die zentrale Einschränkung

Das Jahr-2038-Problem und der Umfang der time64-Umstellung

  • 32-Bit-Anwendungen, die ein 32-Bit-time_t verwenden, können im Jahr 2038 statt der aktuellen Zeit einen -1-Fehler erhalten oder für Dateien kein stat() mehr ausführen
  • Die grundlegende Richtung der Umstellung besteht darin, time_t in einen 64-Bit-Typ zu ändern
    • musl hat die Umstellung bereits vollzogen
    • glibc unterstützt sie als Option
    • Einige Distributionen wie Debian haben bereits umgestellt
  • Bei quellbasierten Distributionen wie Gentoo bauen Nutzer ihr System selbst neu; daher muss die Zeit minimiert werden, in der Pakete in unterschiedlichen ABI-Zuständen verbleiben
  • Das Kernrisiko liegt darin, dass eine Änderung der Breite von time_t die ABI bricht
    • Wenn eine Bibliotheks-API time_t enthält, muss sämtlicher Code, der gegen diese Bibliothek linkt, dieselbe Typbreite verwenden
    • Nur einzelne Teile auf time64 umzustellen, ist nicht sicher

LFS und drei 32-Bit-Unter-ABIs

  • Auf 32-Bit-Architekturen gab es schon länger Probleme mit der Breite dateibezogener Typen
    • off_t wird für Datei-Offsets verwendet
    • ino_t wird für Inode-Nummern verwendet
    • Ursprünglich waren sie 32 Bit breit, wodurch Dateien größer als 2 GiB oder Inode-Nummern außerhalb des 32-Bit-Bereichs problematisch wurden
  • Zur Lösung wurde Large File Support (LFS) eingeführt
    • Dabei werden off_t und ino_t auf 64-Bit-Varianten umgestellt
    • In glibc ist dies bis heute optional
    • Viele Pakete haben upstream LFS aktiviert und ABI-Brüche behandelt, aber das Problem ist damit nicht vollständig gelöst
  • Die time64-Unterstützung von glibc erfordert die Nutzung von LFS, sodass Datei- und Zeitprobleme gemeinsam gelöst werden
  • Auf 32-Bit-Systemen existieren drei Unter-ABIs
    • Bisherige ABI: 32-Bit-Typen
    • LFS: 64-Bit-off_t, 64-Bit-ino_t, 32-Bit-time_t
    • time64: LFS + 64-Bit-time_t
  • Ein einzelner glibc-Build ist mit allen drei Varianten kompatibel, aber Bibliotheken, die diese Typen in ihrer API verwenden, können die drei Varianten nicht miteinander mischen

Wie ABI-Änderungen tatsächlich brechen

  • Wenn time_t von 32 Bit auf 64 Bit wechselt, ändert sich das Layout von Strukturen
    • Wenn in einer Beispielstruktur int a, time_t b, int c in dieser Reihenfolge stehen, unterscheidet sich der Offset von c zwischen 32-Bit-time_t und 64-Bit-time_t
    • Mischt man time32- und time64-Binaries, können falsche Felder gelesen oder geschrieben werden; auch Zugriffe außerhalb der Grenzen werden möglich
  • Auch die Größe von struct stat hängt von der ABI ab
    • Standardwert bei 32-Bit-x86-glibc: 88 Byte
    • LFS: 96 Byte
    • LFS + time64: 108 Byte
  • Selbst ohne Strukturen können Funktionsargumente Probleme verursachen
    • Auf x86 werden Funktionsargumente über den Stack übergeben
    • Wenn eines der Argumente time_t ist, verschiebt sich die Stack-Position der folgenden Argumente
  • In einem Beispielversuch werden Werte beschädigt, wenn ein time32-Programm gegen eine als time64 neu gebaute Bibliothek linkt
    • Die ursprüngliche Ausgabe ist a = 1, ein normaler Zeitwert, c = 3
    • Wird nur die Bibliothek mit -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 neu gebaut, werden b und c falsch interpretiert
  • Derzeit gibt es keinen wirksamen Schutz, der eine solche ABI-Mischung verhindert; dadurch können Laufzeitbrüche und Sicherheitsprobleme entstehen

Warum die Gentoo-Umstellung schwieriger ist

  • Binärdistributionen bauen alle Pakete neu, danach führen Nutzer ein Upgrade in relativ atomaren Schritten durch
    • Drittanbieter-Repositories oder lokal gebaute Programme können Probleme verursachen, insgesamt ist der Prozess aber vergleichsweise sicher
  • Gentoo muss die ABI im laufenden System ändern, während @world neu gebaut wird
    • Zwischen den jeweiligen Rebuilds zweier Pakete können inkompatible ABIs miteinander vermischt werden
    • Wenn einige Rebuilds fehlschlagen, kann das System in einem halb umgestellten Zustand zurückbleiben
    • Wegen zirkulärer Abhängigkeiten kann ein Rebuild abhängiger Pakete Build-Tools beschädigen, wodurch weitere Rebuilds unmöglich werden

Geprüfte Gegenmaßnahmen

  • Diskutiert werden drei Richtungen
    • Änderung von CHOST, dem Plattform-Tupel, um die neue ABI von der bisherigen 32-Bit-ABI zu unterscheiden
    • Änderung des libdir der neuen ABI, damit neu gebaute Bibliotheken getrennt von bestehenden Bibliotheken installiert werden
    • Einführung einer ABI-Unterscheidung auf Binärebene, damit Binaries unterschiedlicher Unter-ABIs nicht miteinander gelinkt werden
  • Die drei Methoden können bis zu einem gewissen Grad unabhängig umgesetzt werden, einige können jedoch voneinander abhängen
  • Die Beispiel-Strings im Text sind möglicherweise nicht die tatsächlich endgültigen Strings einer Lösung

ABI-Unterscheidung per CHOST

  • Das Plattform-Tupel identifiziert die Zielplattform der Toolchain und wird in Gentoo auch verwendet, um ABIs für Multilib-Unterstützung eindeutig zu unterscheiden
  • Das Tupel besteht aus vier Teilen: Architektur, Vendor, Betriebssystem und libc
    • Beispiel: i386-pc-linux-gnu
    • Beispiel: i686-pc-linux-gnu
    • Beispiel: i686-unknown-linux-gnu
  • Bei der Einführung neuer ABIs wurde entweder das Vendor-Feld geändert oder eine ABI-Kennzeichnung an das libc-Feld angehängt
    • Bei der ARM-hardfloat-ABI wurden früher Formen wie armv7a-hardfloat-linux-gnueabi und armv7a-unknown-linux-gnueabihf verwendet
  • Für die time64-ABI gibt es ähnliche Optionen
    • i686-gentoo_t64-linux-gnu
    • i686-pc-linux-gnut64
    • armv7a-gentoo_t64-linux-gnueabihf
    • armv7a-unknown-linux-gnueabihft64
  • Eine Änderung des Tupels dürfte nicht viele Patches erfordern
    • Die GNU toolchain und das GNU build system ignorieren im libc-Feld Inhalte nach gnu
    • Clang benötigt einen Patch, damit je nach Tupel automatisch die richtige ABI gewählt wird

libdir-Änderung und preserved-libs

  • libdir ist der Standardname des Installationsverzeichnisses für Bibliotheken
    • Der übliche Standardwert ist lib
    • Auf Architekturen mit 64-Bit-Gegenstück wird konventionell häufig lib64 verwendet
    • Die x32-ABI auf x86 verwendet libx32, die n32-ABI auf MIPS verwendet lib32
  • Für die 32-Bit-ABI mit time64 wird erwogen, libdir von lib auf einen Wert wie libt64 zu ändern
  • Ein eigenes libdir dient während der Umstellung als Mechanismus, um ABI-Mischungen zu reduzieren
    • Es verringert das Risiko, dass time64-Executables versehentlich gegen time32-Bibliotheken linken
    • Mit Portages preserved-libs-Funktion können time32-Bibliotheken erhalten bleiben
    • Optional kann ein time32+time64-Multilib-Profil angeboten werden, um Kompatibilität mit bestehenden vorab gebauten time32-Anwendungen zu wahren
  • Mit preserved-libs verwenden bestehende Executables bis zu ihrem Rebuild weiter time32-Bibliotheken, während als time64 neu gebaute Bibliotheken im neuen libdir installiert werden
  • Eine libdir-Änderung erfordert Toolchain-Patches
    • glibc kann gesondert behandelt werden, weil derselbe Bibliothekssatz für mehrere Unter-ABIs gültig ist
    • Möglicherweise ist ein separater ld.so nötig, damit .interp von time64-Executables auf den time64-ld.so verweist
  • Für echte Multilib-Unterstützung ist außerdem ein für diese ABI eindeutiges Plattform-Tupel erforderlich

Kennzeichnung von Inkompatibilität auf Binärebene

  • Wenn Binaries unterschiedlicher ABIs gemischt werden, sollten Linker oder dynamischer Loader dies normalerweise verhindern
    • Linkt man ein 64-Bit-Programm gegen eine 32-Bit-Bibliothek, weist der Linker dies mit file in wrong format zurück
    • Auch der dynamische Loader lehnt es mit Fehlern wie wrong ELF class: ELFCLASS32 ab
  • Für bestehende ABI-Unterscheidungen werden mehrere Mechanismen genutzt
    • ELFCLASS32 und ELFCLASS64
    • Machine Identifier wie EM_386 und EM_X86_64
    • Das flags-Feld bei ARM und MIPS
    • Architekturspezifische Attribute Sections
  • Auch für time32 und time64 wird ein ähnlicher Mechanismus benötigt, doch das ist nicht einfach
    • Es scheint keinen wiederverwendbaren allgemeinen Mechanismus zu geben
    • Es braucht eine Lösung für mehrere Architekturen
    • Ein neuer ELF note section mit Toolchain-Unterstützung erscheint als realistischer Kandidat
  • Es muss auch bedacht werden, dass Nutzer den Schutz möglicherweise deaktivieren wollen
    • Vorab gebaute Software ohne Quellcode könnte weiterhin mit Systembibliotheken funktionieren, wenn sie keine APIs aufruft, die time_t verwenden
    • Eine pauschale Blockade könnte schlimmer sein als das Problem selbst
  • Mit einem separaten libdir lässt sich relativ einfach eine nicht fatale QA-Prüfung erstellen
    • time64-Executables werden über .interp unterschieden
    • Es wird geprüft, dass time32-Programme keine Bibliotheken aus libt64 laden
    • Es wird geprüft, dass time64-Programme keine Bibliotheken direkt aus lib laden

Grenzen vorab gebauter 32-Bit-Anwendungen

  • Abseits von Paketen, die aus dem Quellcode gebaut werden, gibt es für x86 und PowerPC Anwendungen, die nur als alte vorab gebaute Binaries verfügbar sind
    • Das betrifft insbesondere proprietäre Software und ältere Spiele
  • Sie sind sowohl von Kompatibilitätsproblemen mit Systembibliotheken als auch vom Jahr-2038-Problem selbst betroffen
  • Für Kompatibilitätsprobleme bietet die bestehende Multilib-Struktur in gewissem Umfang eine Lösung
    • Für die Unterstützung von 32-Bit-Software auf amd64 gibt es bereits ein Multilib-Layout und Mechanismen zum Bauen mehrerer Bibliotheksversionen
    • Dies kann erweitert werden, indem abi_x86_32 und abi_x86_t64 unterschieden werden
    • Es lässt sich ein neues Multilib-x86-Profil erstellen, das beide ABIs unterstützt
  • Das Scheitern von 32-Bit-Programmen selbst nach 2038 bleibt schwieriger
    • Eine Möglichkeit ist, die Systemzeit mit faketime zu steuern
    • Möglich ist auch der Betrieb einer VM, deren Zeit in die Vergangenheit zurückgestellt wurde

Korrektur vom 2024-09-30: libdir allein reicht nicht aus

  • Die ursprüngliche Idee war zu optimistisch; allein durch eine libdir-Änderung lässt sich keine stabile Trennung erreichen
  • Da alle libdirs in ld.so.conf aufgelistet sind, kann man sich nicht darauf verlassen, libdir-Pfade in ld.so hart zu codieren
    • Auch bei einem Custom-LLVM-Prefix werden Pfade bereits angepasst, und auch dieser Fall benötigt Sonderbehandlung
  • Dadurch wird es wahrscheinlicher, dass eine libdir-Änderung von einer Unterscheidung der Binärinkompatibilität abhängen muss
  • Drei grundlegende Ziele müssen erfüllt werden
    • Der dynamische Loader muss time32- und time64-Binaries unterscheiden
    • Alle Binaries ohne explizite time64-Kennzeichnung müssen aus Gründen der Abwärtskompatibilität als time32 gelten
    • Alle neu gebauten Binaries müssen eine explizite time64-Kennzeichnung tragen, einschließlich Binaries, die in Nicht-C-Umgebungen wie Rust gebaut wurden
  • Diese Ziele bedeuten Arbeit auf dem Niveau, mehrere Toolchains mehrerer Sprachen patchen zu müssen
    • Für Gentoo ist es schwer, dies nur lokal zu pflegen; die Zusammenarbeit mehrerer Parteien ist nötig
    • Die Zielarchitekturen gelten oft als Legacy oder werden nicht mehr ausreichend unterstützt
  • Ob andere Toolchains korrekte time64-Executables erzeugen, ist ein eigenes Problem
    • Wenn sie nicht wie C-Programme so angepasst werden, dass sie _TIME_BITS folgen, können sie eine bestimmte time_t-Breite hart codieren und brechen
  • Da alle Binaries ohne explizite time64-Kennzeichnung time32-Bibliotheken verwenden würden, könnte Gentoo keine Drittanbieter-Executables ausführen, die nicht gepatcht wurden, um die korrekte Kennzeichnung zu tragen
  • Es wird auch eine Alternative mit niedrigerem Zielniveau geprüft
    • Allen time64-Executables wird ein RPATH injiziert, der das time64-libdir direkt erzwingt
    • Diese Methode verhindert nicht vollständig, dass der dynamische Loader time32-Bibliotheken verwendet, kann aber die Umstellung ohne große Kompatibilitätsprobleme unterstützen
  • Umgekehrt gibt es auch die Möglichkeit, nicht das time64-libdir dauerhaft zu ändern, sondern das time32-libdir nur vorübergehend umzubenennen
    • Bestehenden Programmen wird ein RPATH injiziert und der libdir-Name geändert
    • Neue time64-Bibliotheken werden im bestehenden libdir installiert
    • Neue time64-Programme tragen keinen RPATH, der time32-Bibliotheken erzwingt
    • Der Vorteil ist, dass die Kompatibilität mit anderen Distributionen erhalten bleibt, die bereits umgestellt haben

Offene Aufgaben

  • Wenn alle drei Lösungsansätze umgesetzt werden, kann Gentoo für 32-Bit-Systeme mit glibc einen saubereren und relativ sicheren Migrationspfad bieten
  • Allerdings gelten diese Lösungen hauptsächlich für Pakete, die aus dem Quellcode gebaut werden
  • Bei vorab gebauten 32-Bit-Anwendungen bleibt das Jahr-2038-Problem bestehen, selbst wenn die ABI-Kompatibilität erhalten wird
  • Das gesamte Design ist noch ein Entwurf und kann sich durch Experimente, Diskussionen und eingereichte Patches weiter ändern

1 Kommentare

 
GN⁺ 2024-09-30
Hacker-News-Kommentare
  • Bei Gentoo gibt es einige Optionen, die im Artikel nicht behandelt wurden; vermutlich wurden sie wegen des hohen Aufwands ausgelassen, der sich aus dem Systemdesign von Gentoo ergibt

    1. Pakete für ein Ziel bauen zu können, ohne sie zu installieren. Der Kernpunkt ist, dass bei Gentoo das Bauen und Installieren eines Pakets ein einziger Schritt ist, sodass man nicht zuerst mehrere voneinander abhängige Komponenten bauen und die Ergebnisse dann atomar installieren kann. Während Updates mit ABI-Änderungen ist das System leicht teilweise kaputt
    2. Das übliche .so-Versioning so zu erweitern, dass auch ABI-Änderungen abhängiger Pakete berücksichtigt werden. Normalerweise enthalten Shared Libraries in Dateinamen und interner Version Versionsnummern, etwa libfoo.so.1.0.0, und Pakete verfolgen ihr eigenes ABI-Breaking. Um 64-Bit-time_t zu unterstützen, müsste man für jede .so eine Versionierungskomponente hinzufügen, die von der abhängigen ABI gesteuert wird. Das Ergebnis wäre ähnlich wie beim im Artikel erwähnten „anderen libdir“, könnte aber als wiederverwendbare Grundlage für künftige ABI-Änderungen dienen, wäre dafür vermutlich deutlich invasiver
    • Für „bauen ohne zu installieren“ scheint ein teilweise gestuftes Update am besten zu passen
      Man könnte mehrere neue Paket-Builds einplanen und in einer Sandbox bauen; neue Kompilierungen würden dann per Union zuerst in die Sandbox schauen und nur bei Bedarf auf das System zurückfallen. Wenn alles gebaut ist, werden die Ergebnisse paketiert und aus der Sandbox ins echte System verschoben. So ließe sich ein komplettes Gentoo-Update transaktionsartig machen, was auch in anderer Hinsicht große Vorteile hätte
    • Gentoo unterstützt 1) bereits, indem man ein fertiges Image mit dem Verzeichnis angibt, in das am Ende installiert werden soll. Dazu muss man einfach ROOT ändern, das normalerweise auf / gesetzt ist
      Man kann das gesamte @system und @world neu bauen und in ein angegebenes Unterverzeichnis installieren und dann auf einmal synchronisieren. Wenn möglich, macht man das besser in einer Live-Session; theoretisch wäre es auch möglich, / in ein Unterverzeichnis des neu installierten Ziels zu bind-mounten, dann per chroot hineinzugehen und auf das echte übergeordnete / zu synchronisieren
      https://devmanual.gentoo.org/ebuild-writing/variables/#root
    • Gentoo behält bereits alte Bibliotheken bei, bis alle abhängigen Komponenten aktualisiert sind; daher könnte man das Problem lösen, indem man ABI-Änderungen in Architektur und SONAME kodiert, also genau dort, wo ABI-Änderungen eigentlich festgehalten werden sollten
  • Die Art, wie Mac OS X mit off_t und ino_t umging, könnte ein Hinweis sein. Bestehende Aufrufe und Strukturen behielten ihr Verhalten, und es wurden neue Aufrufe und Typen mit dem Suffix 64 hinzugefügt; per Präprozessor-Makro konnte ausgewählt werden, worauf tatsächlich verwiesen wird, auch wenn das nur selten direkt geschrieben wurde
    Stattdessen sind OS und SDK versioniert, und beim Build kann man angeben, welche älteste OS-Version die Binärdatei noch unterstützen muss. Die Header wählten darauf basierend automatisch die passenden Makros aus, und Kommentare zu neuen bzw. veralteten APIs erzeugten über denselben Mechanismus Weak Linking oder Warnungen. Anfangs lief das über den Präprozessor, aber heute versteht der Compiler laut Apple die API-Verfügbarkeit wesentlich genauer, sodass so etwas wohl auch auf anderen Plattformen möglich wäre

    • Das zentrale Problem, das der Artikel erklärt, wird dadurch nicht gelöst. Anwendungen mit unterschiedlichen „Ziel-OS-Versionen beim Kompilieren“ lassen sich dann nicht mehr miteinander linken
      Selbst wenn sie auf OS v.B laufen, kann eine Anwendung X, die für OS v.B deklariert wurde, möglicherweise nicht mit einer Anwendung Y linken, die für OS v.A deklariert wurde. Tatsächlich ist das sehr nah an dem, was fast alle Plattformen ohnehin schon tun; andersherum würde die Binärkompatibilität mit bestehenden Programmen sofort brechen
    • Andere Bibliotheken als glibc würden Funktionen wahrscheinlich nicht in mehreren Varianten je nach off_t-Größe definieren, und es gäbe auch keinen Schalter, mit dem Header transparent den richtigen Funktionssatz entsprechend der vom Clientprogramm gewünschten Typgröße auswählen
      Trotzdem betont der Artikel, dass time_t ein größeres Problem ist als off_t. Ein plausibler Grund ist, dass time_t viel weiter verbreitet ist. off_t ist ein POSIX-Typ, der an relativ wenigen Schnittstellen beteiligt ist, während time_t zu ISO C gehört und überall verwendet wird. Außerdem gehen viele C-Programme davon aus, dass time_t ein ganzzahliger Typ mit derselben Breite wie int ist; solche Annahmen über off_t sind seltener
    • Klingt wie eine elegante Lösung, liest sich in der Praxis aber wie ein schrecklicher Hack. Typfreie Makros sind ein Albtraum, mit dem ich nie wieder zu tun haben möchte
    • Funktioniert nur, wenn man die gesamte Plattform dazu zwingen kann mitzugehen. Es ist eine gute Lösung, aber man muss die C-Bibliothek kontrollieren. Gentoo kann nicht kontrollieren, was libc tut, und Nutzer können GNU libc, musl oder etwas anderes verwenden
  • Auch Debian hatte große Schmerzen damit. Einige sind möglicherweise ausgebrannt, und viele zeigten auf eine source-basierte Distribution und sagten: „Dort drüben muss das doch ganz einfach sein“

    • Mich würde Material interessieren, das im Detail zeigt, wie schmerzhaft Debians time64-Umstellung war. Von außen wirkte es relativ reibungslos und ohne große Kontroversen, jedenfalls deutlich besser als zum Beispiel die /usr-Zusammenführung
    • Ich habe die Umstellungen für m68k, powerpc und sh4 gemacht und bei hppa teilweise geholfen; mit Hilfe anderer Debian-Entwickler lebe ich noch
    • Wenn mit „einfach“ gemeint ist, dass man den Nutzern einfach sagt, sie sollen alles auf einmal neu bauen, dann wohl ja
  • Jedes Mal, wenn ich sehe, wie sehr Leute unter diesem Problem leiden, denke ich mir, wie gut es war, dass FreeBSD das damals beim ersten amd64-Port konsequent durchgezogen hat. Man konnte die Basis-Typen des ABI festlegen und entschied sich, in die Zukunft statt in die Vergangenheit zu schauen
    amd64 hatte ein interessantes Merkmal, das diese Arbeit erleichterte. Da 32-Bit-Funktionsargumente bei Funktionsaufrufen automatisch auf 64 Bit gecastet wurden, funktionierte es in der frühen Portierungsphase meist einfach, selbst wenn man 32-Bit-Zeitwerte an Funktionen übergab, die ein 64-Bit-time_t erwarteten. So konnte man kleinere Restarbeiten auf später verschieben
    Es gab damals auch andere 64-Bit-Plattformen, aber kein 64-Bit-time_t, und FreeBSD/amd64 war meines Wissens um 2003–2005 in dieser Familie die erste. Soweit ich mich erinnere, ist auch sparc64 auf 64-Bit-time_t umgestiegen
    Das größte Problem war, dass tzcode damals nicht 64-Bit-sicher war. Der Normalisierungsalgorithmus von struct tm geriet in einen degenerierten Zustand, in dem er versuchte, Tag/Monat/Jahr für time_t(2^62) iterativ zu berechnen. Anstatt tzcode stark umzubauen, wurde es meines Wissens so gehandhabt, dass Daten grob vor 1900 oder nach 10000 einfach fehlschlagen. Vermutlich ist das inzwischen längst upstream behoben
    Über einige Jahre hinweg musste man in Third-Party-Code immer wieder 32-/64-Bit-Verwechslungen bei Zeitwerten beseitigen, die dadurch entstanden, dass Datenstrukturen in Dateien oder im Netzwerk int/long/time_t unsauber behandelten, aber insgesamt war das kein großes Problem. Dass man von Tag eins an 64-Bit-time_t verwendete, half, die meisten Probleme zu vermeiden, und ganz am Anfang ist so etwas leicht. Linux hat diese große Chance beim Start des amd64/x86_64-Ports verpasst
    Zusätzlich ist anzumerken, dass man 64-Bit-ino_t damals nicht zu Ende gebracht hat. 32-Bit-Inode-Nummern waren an sehr vielen Stellen offengelegt, etwa in On-Disk-Strukturen von Dateisystemen, in UFS-Verzeichnisstrukturen usw. Als FreeBSD/amd64 noch eine nachrangige Plattform war, gab es keinen praktikablen Weg, das von Anfang an zu erledigen, ohne andere Tier-1-Architekturen stark durcheinanderzubringen. An der Arbeit wurde zweimal angesetzt, aber am Ende hat jemand anders sie fertiggestellt und dabei auch zu kurze Konstanten wie die Pfadlänge von Mountpoints korrigiert

    • Soweit ich weiß, verwendeten alle 64-Bit-Linux-Ports von Anfang an 64-Bit-time_t, off_t und ino_t. Das aktuelle Problem ist die Umstellung von 32-Bit-Linux auf 64-Bit-time_t
    • FreeBSD ist mit off_t noch mutiger umgegangen und hat es bereits seit 2.0 auf 64 Bit gesetzt. In den 32-Bit-Versionen von Linux gibt es noch Spuren der alten Größe
      Ich verstehe den Teil mit dem automatischen Cast von 32-Bit-Funktionsargumenten auf 64 Bit so, dass das nur für vorzeichenlose Argumente gilt. Wenn in %edi geladen wird, wird der obere Teil von %rdi gelöscht. Die SysV-ABI-Spezifikation für x86-64 sagt nicht, dass alle Register- oder Stackwerte auf einen vollständigen 64-Bit-Wert erweitert werden, und selbst die Anmerkung zu Booleans besagt nur, dass das niederwertige Byte Bedeutung hat, was darauf hindeutet, dass dies keine allgemeine Regel ist
    • Falls gemeint ist, dass FreeBSD beim Erscheinen von amd64 auch time_t auf i386 auf 64 Bit portiert hat, wäre das ziemlich überraschend. Ich frage mich, ob andere 32-Bit-Architekturen wie Motorola 68000 oder sparc32 ebenfalls auf 64-Bit-time_t umgestellt wurden
  • Auf einem alten großen 32-Bit-Unix-System habe ich einmal, um zukünftige Datumswerte verarbeiten zu können, libc-Funktionen mit signiertem 32-Bit-time_t durch Varianten für vorzeichenloses 32-Bit-time_t ersetzt. Damit gewann man nach 2038 noch weitere 68 Jahre, und bis dahin werde ich ohnehin nicht mehr da sein
    Der Nachteil ist, dass sich damit keine Daten vor der Unix-Epoche 1970 darstellen lassen, aber für ein Terminplanungssystem war das kein Problem. Wenn vergangene Daten wichtig sind, könnte man die Epoche um einige Jahrzehnte verschieben oder die Zeitauflösung von 1 Sekunde auf 2 Sekunden reduzieren. Beides hat jeweils subtile Probleme, also hängt es vom Anwendungsfall ab

    • Wenn man das ganze System von signed auf unsigned umstellen kann, frage ich mich, warum man nicht gleich auf 64 Bit umgestellt hat
  • In der ursprünglichen BSD-Manpage hatte der Abschnitt „Bugs“ von tunefs den berühmten Witz „You can tune a file system, but you can't tune a fish.“, und laut „Expert C Programming“ stand im Quelltext dieser Manpage neben dem Witz folgender Kommentar
    „Wenn du das entfernst, wird dir der UNIX-Dämon von jetzt an bis zu dem Zeitpunkt, an dem time_t überläuft, auf den Fersen bleiben.“
    Als dieser Satz in den 70ern geschrieben wurde, muss 2038 offensichtlich unvorstellbar weit in der Zukunft gelegen haben
    https://progforperf.github.io/Expert_C_Programming.pdf

  • Mein stärkster Eindruck ist: Respekt für den Aufwand, aber aus Nutzersicht möchte ich dieses Problem einfach beenden, indem ich zu einer nicht quellbasierten Distribution wie Debian wechsle

    • Die Schwierigkeit bei quellbasierten Distributionen scheint daher zu kommen, dass man versucht, In-Place-Upgrades durchzuführen, während ABI-inkompatible Änderungen vorgenommen werden. Deshalb kann der Wechsel auf eine völlig andere Distribution genauso disruptiv sein wie eine saubere Neuinstallation von Gentoo mit dem neuen ABI, vielleicht sogar weniger zeitaufwendig, aber mindestens ähnlich einschneidend
    • Auch unter Gentoo gibt es einen einfachen Weg, das zu lösen. Man bootet etwa von USB, führt auf den Partitionen für / und /usr mkfs.ext4 oder das verwendete Dateisystem aus, mountet sie, entpackt stage3, geht per chroot hinein und führt emerge $all-my-packages-that-where-installed-before-mkfs aus
      Statt schrittweise zu aktualisieren, kann man also eine neue Gentoo-Kopie installieren
    • Die Unterscheidung, man müsse nur zu einer nicht quellbasierten Distribution wechseln, ist etwas subtiler. Eine quellbasierte Distribution wie NixOS hat dieses Problem nicht. Der Kernpunkt ist weniger, ob aus Quelltext gebaut wird, sondern wie Gentoo Pakete baut und installiert
      Mit Third-Party-Closed-Source-Software kann es auch auf binären Systemen weiterhin Probleme geben. Ebenso können Probleme bei First-Party-Paketen auftreten, die als getrennte Schritte separat installiert werden
  • Ich bin zwar kein C-Experte, dachte aber, dass Typ-Aliasse wie off_t gerade eingeführt wurden, damit man sie später ändern kann. Es wirkt jedoch so, als würde das nicht wirklich eindeutig funktionieren, und ich frage mich, ob ich das falsch verstanden habe.

    • Das ist der Unterschied zwischen Quellkompatibilität und Binärkompatibilität. Wenn man ein typedef wie off_t verwendet, muss man den Code normalerweise nicht neu schreiben, aber alles, was diesen Typ verwendet, muss neu kompiliert werden.
    • Bis zu einem gewissen Grad funktioniert das, aber für quellbasierte Distributionen passt es schlecht. Wenn man nach der Änderung der off_t-Definition @world atomar neu bauen könnte, gäbe es kein Problem, aber quellbasierte Distributionen bauen @world nicht atomar neu, sondern kompilieren die Pakete nacheinander neu.
      Dann kann es passieren, dass libc.so bereits 64-Bit-off_t verwendet, während gcc noch gegen 32-Bit-off_t gebaut ist, sodass gcc hängen bleibt. Auch Pakete wie bash, coreutils, make und binutils, die für den Neuaufbau von @world nötig sind, können kaputtgehen, und an diesem Punkt ist man blockiert. Deshalb erfordern solche Upgrades besondere Vorsicht.
    • Das ist nur der erste Teil des Puzzles, vielleicht sogar nur ein halber. Wie im Artikel beschrieben, verschwindet die Abstraktion in dem Moment, in dem off_t in einer Struktur landet, bei Funktionsaufrufen verwendet wird oder in ein Protokoll eingebaut ist, und die tatsächliche Größe wird wichtig.
      Mischt man beim Laden von Bibliotheken oder bei der Kommunikation über ein Protokoll alten und neuen Code, geraten die Offsets durcheinander und es kommt zu Abstürzen. Letztlich ist die Umstellung sehr schmerzhaft, weil alle Programme in „Legacy“ und „portiert oder zumindest geprüft“ aufgeteilt werden müssen.
    • Typ-Aliasse machen es nur auf Quellcode-Ebene einfacher. Sie sind keine echte, erst recht keine vollständige Abstraktion. Wenn man zum Beispiel den internen Typ in einen Fließkommatyp ändert, verändert sich die Bedeutung stark und das wird im Nutzer-Code vollständig sichtbar.
      Selbst ein Wechsel zu einem größeren Typ mit ähnlicher Bedeutung kann etwas kaputtmachen. Ein einfaches Beispiel ist Struct-Padding, und es gibt auch viele Anwendungsfälle, in denen Pointer in Integer umgewandelt und wieder zurückverwandelt werden, sodass Änderungen an der internen Darstellung zwangsläufig Dinge zerstören können. Ob das gute Praxis ist, steht auf einem anderen Blatt, aber ungewöhnlich ist es nicht. Der Kernpunkt ist ABI-Kompatibilität.
    • Es funktioniert, aber bei ABI-Änderungen besteht das Problem darin, dass man beim Umstellen alles gleichzeitig umstellen muss. Im Grunde gibt es keinen Schutzmechanismus, der verhindert, dass man Bibliotheken linkt, die mit 32-Bit-off_t gebaut wurden, mit solchen, die mit 64-Bit-off_t gebaut wurden, und das resultierende Verhalten kann äußerst unvorhersehbar sein.
  • Im Beispiel-Struct wurde gesagt, dass der Offset von c bei 32-Bit-time_t 8 und bei einem 64-Bit-Typ 12 sei, aber eigentlich müsste es nicht 16 sein? b müsste doch auf 64 Bit ausgerichtet werden, also müsste zwischen a und b Padding eingefügt werden. Das würde den Punkt, den der Autor machen will, eher noch stärker unterstreichen.

    • Die meisten x86-ABIs erzwingen Padding für 64-Bit-Typen nicht, weil es damals noch keine 64-Bit-Loads gab.
  • Wenn man sich all das ansieht, dann hat selbst die seltsame Zeitdarstellung von Windows — also 64 Bit in 100-ns-Schritten gezählt seit dem 1. Januar 1601, 00:00 GMT, im gregorianischen Kalender — einen kleinen Vorteil. Die Auflösung ist ebenfalls hervorragend, und das wird vermutlich funktionieren, bis die gesamte Galaxie erobert ist.