Los riesgos de la transición a `time_t` de 64 bits
(blogs.gentoo.org)- En sistemas glibc de 32 bits, después de 2038 pueden fallar la consulta de la hora actual o las llamadas a
stat(), por lo que Gentoo necesita una ruta para migrar de forma segura atime_tde 64 bits - El soporte time64 de glibc debe usarse junto con Large File Support (LFS), y en entornos de 32 bits coexisten el ABI tradicional, el ABI LFS y el ABI LFS+time64
- Cuando
time_taparece en APIs, estructuras o argumentos de función, el cambio en el ancho del tipo provoca una ruptura de ABI; mezclar binarios time32 y time64 puede causar fallas en tiempo de ejecución y riesgos de seguridad - Como Gentoo es una distribución basada en código fuente, durante una reconstrucción de
@worldpueden quedar sistemas a medio migrar por fallas o dependencias circulares - Tras la corrección del 2024-09-30, quedó claro que cambiar solo
libdirno basta, y la restricción clave pasa a ser una marca time64 que incluya el cargador dinámico y varias toolchains de distintos lenguajes
El problema de 2038 y el alcance de la transición a time64
- Las aplicaciones de 32 bits que usan
time_tde 32 bits pueden recibir el error-1en lugar de la hora actual en 2038, o no poder ejecutarstat()sobre archivos - La dirección básica de la transición es cambiar
time_ta un tipo de 64 bits- musl ya hizo la transición
- glibc lo soporta como opción
- algunas distribuciones como Debian ya hicieron el cambio
- En distribuciones basadas en código fuente como Gentoo, los usuarios recompilan el sistema por sí mismos, así que hay que reducir el tiempo durante el cual los paquetes quedan en estados ABI distintos
- El riesgo central es que cambiar el ancho de
time_trompe el ABI- si una API de biblioteca contiene
time_t, todo el código que enlaza con esa biblioteca debe usar el mismo ancho de tipo - no es seguro cambiar solo una parte a time64
- si una API de biblioteca contiene
LFS y los tres sub-ABI de 32 bits
- En arquitecturas de 32 bits ya existían desde antes problemas con el ancho de tipos relacionados con archivos
off_tse usa para offsets de archivosino_tse usa para números de inode- originalmente tenían 32 bits, así que aparecían problemas con archivos mayores a 2GiB o con números de inode fuera del rango de 32 bits
- Para resolverlo se introdujo Large File Support (LFS)
- cambia
off_teino_ta variantes de 64 bits - en glibc sigue siendo opcional incluso hoy
- muchos paquetes activaron LFS upstream y manejaron la ruptura de ABI, pero el problema no quedó resuelto por completo
- cambia
- El soporte time64 de glibc requiere usar LFS, así que el diseño resuelve a la vez los problemas de tamaño de archivo y de tiempo
- En sistemas de 32 bits existen tres sub-ABI
- ABI tradicional: tipos de 32 bits
- LFS:
off_tde 64 bits,ino_tde 64 bits,time_tde 32 bits - time64: LFS +
time_tde 64 bits
- Una misma compilación de glibc es compatible con las tres variantes, pero las bibliotecas que usan estos tipos en su API no pueden mezclar entre sí las tres variantes
Cómo se rompe realmente el ABI
- Cuando
time_tcambia de 32 a 64 bits, cambia la disposición de las estructuras en memoria- en una estructura de ejemplo con
int a,time_t b,int cen ese orden, el offset decdifiere entretime_tde 32 bits y de 64 bits - si se mezclan binarios time32 y time64, pueden leer o escribir campos equivocados, e incluso acceder fuera de rango
- en una estructura de ejemplo con
- El tamaño de
struct stattambién cambia según el ABI- valor predeterminado de glibc en x86 de 32 bits: 88 bytes
- LFS: 96 bytes
- LFS + time64: 108 bytes
- Incluso sin usar estructuras, los argumentos de función causan problemas
- en x86 los argumentos de función se pasan por la pila
- si uno de los argumentos es
time_t, cambia la posición en la pila de los argumentos que vienen después
- En el experimento de ejemplo, un programa time32 enlazado con una biblioteca recompilada como time64 termina con valores corruptos
- la salida original era
a = 1, un valor de tiempo normal yc = 3 - si solo la biblioteca se recompila con
-D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64,bycse interpretan incorrectamente
- la salida original era
- Hoy no existe una protección efectiva para impedir esta mezcla de ABI, por lo que puede haber rupturas en tiempo de ejecución y problemas de seguridad
Por qué la transición en Gentoo es más difícil
- En distribuciones binarias, todos los paquetes se recompilan primero y luego el usuario actualiza en una etapa relativamente atómica
- puede haber problemas con repositorios de terceros o programas compilados localmente, pero en conjunto el proceso es relativamente seguro
- Gentoo tiene que cambiar el ABI en el mismo lugar mientras recompila
@world- entre la recompilación de dos paquetes pueden mezclarse ABI incompatibles
- si algunas recompilaciones fallan, el sistema puede quedar en un estado a medio migrar
- por dependencias circulares, recompilar paquetes necesarios puede romper las herramientas de compilación y volver imposible continuar con la recompilación
Medidas de mitigación en evaluación
- Hay tres líneas principales en discusión
- cambiar
CHOST, el triplete de plataforma, para distinguir el nuevo ABI del ABI tradicional de 32 bits - cambiar el
libdirdel nuevo ABI para instalar las bibliotecas recompiladas por separado de las bibliotecas existentes - introducir una distinción de ABI a nivel binario para impedir que binarios de sub-ABI distintos se enlacen entre sí
- cambiar
- Los tres métodos pueden implementarse con cierto grado de independencia, aunque algunos pueden depender de otros
- Las cadenas de ejemplo del artículo podrían no coincidir con las cadenas reales de la solución final
Distinguir el ABI con CHOST
- El triplete de plataforma identifica la plataforma objetivo de la toolchain, y en Gentoo también se usa para distinguir de forma única el ABI dentro del soporte multilib
- El triplete se compone de cuatro partes: arquitectura, vendor, sistema operativo y libc
- ejemplo:
i386-pc-linux-gnu - ejemplo:
i686-pc-linux-gnu - ejemplo:
i686-unknown-linux-gnu
- ejemplo:
- Al introducir un ABI nuevo, se ha usado cambiar el campo vendor o añadir la marca del ABI al campo libc
- en el ABI hardfloat de ARM se usaron formas como
armv7a-hardfloat-linux-gnueabiyarmv7a-unknown-linux-gnueabihf
- en el ABI hardfloat de ARM se usaron formas como
- Para el ABI time64 hay opciones similares
i686-gentoo_t64-linux-gnui686-pc-linux-gnut64armv7a-gentoo_t64-linux-gnueabihfarmv7a-unknown-linux-gnueabihft64
- El cambio de triplete no parece requerir demasiados parches
- la toolchain GNU y el sistema de build GNU ignoran lo que viene después de
gnuen el campo libc - Clang sí necesitaría parches para seleccionar automáticamente el ABI correcto según el triplete
- la toolchain GNU y el sistema de build GNU ignoran lo que viene después de
Cambio de libdir y preserved-libs
libdires el nombre base del directorio donde se instalan bibliotecas- el valor predeterminado habitual es
lib - en arquitecturas con soporte de 64 bits suele usarse por convención
lib64 - el ABI x32 de x86 usa
libx32, y el ABI n32 de MIPS usalib32
- el valor predeterminado habitual es
- Se evalúa cambiar
libdirdeliba algo comolibt64para el ABI de 32 bits con time64 - Un
libdirseparado ayudaría a reducir la mezcla de ABI durante la transición- reduce el riesgo de que ejecutables time64 enlacen por error con bibliotecas time32
- permite conservar bibliotecas time32 mediante la función preserved-libs de Portage
- opcionalmente se podría ofrecer un perfil multilib time32 + time64 para mantener compatibilidad con aplicaciones precompiladas existentes de time32
- Con preserved-libs, los ejecutables existentes seguirían usando bibliotecas time32 hasta ser recompilados, mientras que las bibliotecas recompiladas como time64 se instalarían en el nuevo
libdir - Cambiar
libdirrequiere parches en la toolchain- glibc podría manejarse de forma especial porque el mismo conjunto de bibliotecas sirve para varios sub-ABI
- podría hacer falta un
ld.soseparado para que el.interpde los ejecutables time64 apunte alld.socorrespondiente a time64
- Para un soporte multilib adecuado, también hace falta un triplete de plataforma específico para ese ABI
Marcar la incompatibilidad a nivel binario
- Cuando se mezclan binarios de ABI distintos, normalmente el linker o el cargador dinámico deberían bloquearlo
- si un programa de 64 bits enlaza con una biblioteca de 32 bits, el linker lo rechaza con
file in wrong format - el cargador dinámico también lo rechaza con errores como
wrong ELF class: ELFCLASS32
- si un programa de 64 bits enlaza con una biblioteca de 32 bits, el linker lo rechaza con
- Para distinguir ABI existentes se usan varios mecanismos
ELFCLASS32yELFCLASS64- identificadores de máquina como
EM_386yEM_X86_64 - el campo flags en ARM y MIPS
- secciones de atributos específicas de cada arquitectura
- Para time32 y time64 hace falta un mecanismo similar, pero no es sencillo
- no parece existir un mecanismo general reutilizable
- hace falta una solución que funcione en varias arquitecturas
- una opción realista sería añadir una nueva sección ELF note e implementar soporte en la toolchain
- También hay que considerar que el usuario podría desactivar la protección
- si software precompilado sin código fuente no llama a APIs que usan
time_t, podría seguir funcionando con las bibliotecas del sistema - una medida que lo impida siempre podría ser peor que el problema original
- si software precompilado sin código fuente no llama a APIs que usan
- Si se usa un
libdirseparado, se puede hacer una verificación QA no fatal relativamente simple- se distingue a los ejecutables time64 mediante
.interp - se verifica que programas time32 no carguen bibliotecas desde
libt64 - se verifica que programas time64 no carguen bibliotecas directamente desde
lib
- se distingue a los ejecutables time64 mediante
Límites de las aplicaciones precompiladas de 32 bits
- Aparte de los paquetes compilados desde código fuente, en x86 y PowerPC existen aplicaciones que solo se distribuyen como binarios precompilados antiguos
- esto aplica especialmente a software propietario y juegos viejos
- Esas aplicaciones enfrentan tanto problemas de compatibilidad con las bibliotecas del sistema como el propio problema de 2038
- Para los problemas de compatibilidad, la estructura multilib actual ofrece cierta solución
- en amd64 ya existen mecanismos para soportar software de 32 bits, con un layout multilib y compilación de múltiples versiones de bibliotecas
- eso puede extenderse distinguiendo
abi_x86_32yabi_x86_t64 - se podría crear un nuevo perfil x86 multilib que soporte ambos ABI
- El fallo de los programas de 32 bits en sí después de 2038 sigue siendo un problema más difícil
- es posible controlar la hora del sistema con faketime
- también es posible ejecutar una VM con el reloj retrocedido
Corrección del 2024-09-30: solo con libdir no alcanza
- La idea inicial era demasiado optimista, y cambiar solo
libdirno permite una separación estable - Como todos los
libdiraparecen listados enld.so.conf, no se puede depender de hardcodear rutas delibdirdentro deld.so- incluso en prefijos LLVM personalizados ya se ajustan rutas, y ese caso también requeriría manejo especial
- Por eso, el cambio de
libdirprobablemente tenga que depender de una distinción de incompatibilidad binaria - Hay tres objetivos básicos que deben cumplirse
- el cargador dinámico debe distinguir entre binarios time32 y time64
- todos los binarios sin una marca time64 explícita deben considerarse time32 por compatibilidad hacia atrás
- todos los binarios nuevos deben llevar una marca time64 explícita, incluyendo binarios construidos en entornos no C como Rust
- Esto implica un trabajo del nivel de parchear múltiples toolchains de múltiples lenguajes
- es difícil que Gentoo lo mantenga solo de forma local y hace falta cooperación de varias partes
- las arquitecturas objetivo suelen considerarse legacy o ya no reciben suficiente soporte
- También queda la duda de si otras toolchains producirán ejecutables time64 correctos
- si no se ajustan para respetar
_TIME_BITScomo hace un programa en C, podrían fijar un ancho específico paratime_ty romperse
- si no se ajustan para respetar
- Como todos los binarios sin marca explícita time64 usarían bibliotecas time32, Gentoo dejaría de poder ejecutar binarios de terceros que no hayan sido parchados para llevar la marca correcta
- También se evalúan alternativas con objetivos más modestos
- inyectar RPATH en todos los ejecutables time64 para forzar directamente el
libdirde time64 - este método no impide por completo que el cargador dinámico use bibliotecas time32, pero podría facilitar la transición sin grandes problemas de compatibilidad
- inyectar RPATH en todos los ejecutables time64 para forzar directamente el
- En sentido inverso, también se contempla no cambiar permanentemente el
libdirtime64 y cambiar temporalmente ellibdirtime32- se inyecta RPATH en programas existentes y se renombra el
libdir - las nuevas bibliotecas time64 se instalan en el
libdirtradicional - los nuevos programas time64 no tendrían un RPATH que fuerce bibliotecas time32
- esto tendría la ventaja de mantener compatibilidad con otras distribuciones que ya hicieron la transición
- se inyecta RPATH en programas existentes y se renombra el
Tareas pendientes
- Si se implementan las tres soluciones, se podría ofrecer una ruta de transición más limpia y relativamente segura para sistemas Gentoo de 32 bits con glibc
- Aun así, estas soluciones aplican sobre todo a paquetes compilados desde código fuente
- Las aplicaciones precompiladas de 32 bits seguirán teniendo el problema de 2038, incluso si se mantiene la compatibilidad de ABI
- El diseño completo sigue siendo un borrador y puede cambiar conforme avancen los experimentos, las discusiones y el envío de parches
1 comentarios
Opiniones de Hacker News
En Gentoo hay algunas opciones que el artículo no cubre, y parece que se omitieron porque, por el diseño del sistema de Gentoo, implican mucho trabajo
.sohabitual para reflejar también los cambios de ABI de los paquetes dependientes. Normalmente, las bibliotecas compartidas llevan números de versión tanto en el nombre de archivo como en la versión interna, comolibfoo.so.1.0.0, y el paquete rastrea las rupturas de su propia ABI. Para soportartime_tde 64 bits, habría que agregar un componente de versión controlado por la ABI de dependencia de cada.so. El resultado sería parecido a lo que el artículo llama “usar otro libdir”, pero aunque podría convertirse en una base reutilizable para futuros cambios de ABI, probablemente sería mucho más invasivoSe podrían programar varias compilaciones de paquetes nuevos y compilarlas en un sandbox; luego, las compilaciones nuevas mirarían primero el sandbox mediante una unión y harían fallback al sistema. Cuando todo esté compilado, se empaquetan los resultados y se mueven del sandbox al sistema real. Así toda la actualización de Gentoo podría volverse transaccional, lo que también traería grandes beneficios en otros aspectos
ROOT, que normalmente se configura como/Se puede recompilar todo
@systemy@world, instalarlo en el subdirectorio indicado y luego sincronizarlo todo de una vez. Si es posible, conviene hacerlo desde una sesión live; en teoría también sería posible hacer un bind mount de/en un subdirectorio de la nueva ubicación instalada, entrar con chroot y sincronizar hacia el/superior realhttps://devmanual.gentoo.org/ebuild-writing/variables/#root
La forma en que Mac OS X manejó
off_teino_tpodría servir de pista. Las llamadas y estructuras existentes mantuvieron su comportamiento, se agregaron nuevas llamadas y tipos con el sufijo64, y se podía elegir el destino real de las referencias mediante macros del preprocesador, aunque rara vez se usaban directamenteEn cambio, el OS y el SDK están versionados, y al compilar se puede especificar la versión más antigua del OS en la que debe ejecutarse el binario. Con base en eso, los headers elegían automáticamente las macros adecuadas, y las anotaciones de API nuevas/deprecadas también usaban el mismo mecanismo para generar enlaces débiles o advertencias. Al principio se hacía con el preprocesador, pero hoy el compilador entiende de forma más sofisticada lo que Apple llama disponibilidad de API, así que parece posible aplicar el mismo enfoque en otras plataformas
Aunque estén ejecutándose en OS v.B, la aplicación X declarada para OS v.B podría no poder enlazarse con la aplicación Y declarada para OS v.A. En realidad, este enfoque se parece bastante a lo que ya hacen casi todas las plataformas; hacerlo de otra forma rompería de inmediato la compatibilidad con binarios existentes
off_t, y tampoco habrá un switch en los headers que elija de forma transparente el conjunto correcto de funciones según el tamaño de tipo que quiera el programa clienteAun así, el artículo enfatiza que
time_tes un problema mayor queoff_t. Una razón plausible es quetime_testá mucho más extendido.off_tes un tipo POSIX que participa en relativamente pocas interfaces, mientras quetime_tforma parte de ISO C y se usa por todos lados. Además, mucho código en C asume quetime_tes un tipo entero del mismo ancho queint, mientras que esas suposiciones sobreoff_tson menos comunesDebian también fue muy doloroso. Es posible que algunos hayan terminado quemados, y mucha gente apuntaba a las distribuciones basadas en código fuente diciendo “para ellos será muy fácil”
/usrCada vez que veo a alguien sufrir con este tipo de problemas, siento que fue una gran suerte que en los primeros ports de FreeBSD a amd64 se haya impulsado este cambio. Pudimos definir los tipos básicos de la ABI y decidimos mirar hacia el futuro en vez de al pasado.
amd64 tenía una característica interesante que facilitó este trabajo. Como los argumentos de función de 32 bits se convertían automáticamente a 64 bits durante las llamadas a funciones, si se pasaba un entero de tiempo de 32 bits a una función que esperaba un
time_tde 64 bits, en su mayoría simplemente funcionaba durante el trabajo inicial de la plataforma. Así que los detalles menores se podían dejar para después.En ese momento ya había otras plataformas de 64 bits, pero no tenían
time_tde 64 bits, y FreeBSD/amd64 fue la primera de esa familia alrededor de 2003~2005. Según recuerdo, sparc64 también migró atime_tde 64 bits.El mayor problema era que en aquel entonces tzcode no era seguro para 64 bits. El algoritmo de normalización de
struct tmcaía en una condición degenerada en la que intentaba calcular iterativamente el día/mes/año detime_t(2^62). En vez de cambiar tzcode de forma importante, recuerdo que se hizo que fallara más o menos antes del año 1900 o después del año 10000. Es muy probable que upstream lo haya corregido hace mucho.Durante algunos años estuvimos jugando al whack-a-mole con confusiones de tiempo de 32/64 bits en código de terceros, causadas por tratar de forma descuidada
int/long/time_ten estructuras de datos de archivos o de red, pero en general no fue un gran problema. Usartime_tde 64 bits desde el primer día evitó la mayoría de los problemas, y hacerlo desde el principio fue fácil. Linux perdió una gran oportunidad de hacer lo mismo cuando inició el port a amd64/x86_64.Además, en ese momento no pudimos terminar lo de
ino_tde 64 bits. Los números de inode de 32 bits estaban expuestos en muchísimos lugares, como las estructuras on-disk del sistema de archivos y la estructura de directorios de UFS. Cuando FreeBSD/amd64 todavía era una plataforma de menor categoría, no había una forma realista de resolverlo desde el principio sin sacudir mucho otras arquitecturas tier-1. Hice el trabajo dos veces, pero al final otra persona lo terminó, y de paso se corrigieron constantes que eran demasiado cortas, como la longitud de la ruta de los mountpoints.time_t,off_teino_tde 64 bits. El problema actual es migrar Linux de 32 bits atime_tde 64 bits.off_t, que es de 64 bits desde la versión 2.0. En las versiones de Linux de 32 bits todavía quedan rastros de los tamaños antiguos.Entiendo que la parte de que los argumentos de función de 32 bits se convierten automáticamente a 64 bits durante la llamada solo funciona con argumentos sin signo. Es porque al cargarlos en
%edise borra la parte superior de%rdi. La especificación de la SysV ABI para x86-64 no dice que todos los valores en registros o en la pila se extiendan al valor completo de 64 bits, y la nota sobre booleanos también dice algo como que solo importa el byte inferior, lo que sugiere que esa es la regla general.time_tde i386 a 64 bits, eso me sorprende bastante. Me pregunto si otras arquitecturas de 32 bits, como Motorola 68000 o sparc32, también migraron atime_tde 64 bits.En un viejo y gran sistema Unix de 32 bits, para manejar fechas futuras, una vez reemplacé las funciones de libc con
time_tde 32 bits con signo por sus equivalentes contime_tde 32 bits sin signo. Con eso ganamos 68 años más después de 2038, y para entonces yo ya no estaré.La desventaja es que no se pueden representar fechas anteriores a 1970, el Unix epoch, pero como era un sistema de calendario, no importaba. Si las fechas pasadas fueran importantes, también se podría mover el epoch varias décadas o reducir la resolución temporal de 1 segundo a 2 segundos. Cada opción tiene problemas sutiles, así que depende del caso de uso.
En la página de manual original de BSD, la sección “Bugs” de
tunefstenía el famoso chiste “You can tune a file system, but you can't tune a fish.”, y según “Expert C Programming”, en el código fuente de esa página de manual había este comentario junto al chiste:“Si quitas esto, el demonio de UNIX te perseguirá en cuatro patas desde ahora hasta que
time_thaga wrap around.”Cuando se escribió esa frase en los años 70, 2038 sin duda debía parecer un futuro inimaginablemente lejano.
https://progforperf.github.io/Expert_C_Programming.pdf
Lo que más me queda es que, aunque respeto el esfuerzo, como usuario quisiera pasarme a una distribución no basada en código fuente como Debian y dar por terminado este problema.
mkfs.ext4o el sistema de archivos que uses sobre las particiones/y/usr, las montas, desempaquetas stage3, entras al chroot y ejecutasemerge $all-my-packages-that-where-installed-before-mkfs.En vez de actualizar gradualmente, puedes instalar una copia nueva de Gentoo.
Si hay software de terceros de código cerrado, incluso en un sistema binario todavía pueden aparecer problemas. También pueden aparecer en paquetes first-party que se instalan por separado como un paso independiente.
No soy experto en C, pero pensaba que alias de tipos como
off_tse habían introducido para poder cambiarlos más adelante. Pero parece que claramente no funciona así; me pregunto si lo entendí mal.off_t, normalmente no tienes que reescribir el código, pero sí tienes que recompilar todo lo que use ese tipo.off_t, pudieras reconstruir@worldde forma atómica, no habría problema; pero las distribuciones basadas en código fuente no reconstruyen@worldde forma atómica, sino paquete por paquete.Entonces puede pasar que
libc.souse unoff_tde 64 bits, mientrasgccfue compilado suponiendo unoff_tde 32 bits, y quegccse detenga. También podrían romperse paquetes necesarios para reconstruir@world, comobash,coreutils,makeobinutils, y en ese punto quedas bloqueado. Por eso estas actualizaciones requieren cuidado.off_tentra en una estructura, se usa en una llamada a función o se integra en un protocolo, la abstracción desaparece y el tamaño real importa.Si mezclas código viejo y nuevo al cargar una biblioteca o comunicarte mediante un protocolo, los offsets empiezan a quedar corridos y comienzan los choques. Al final, la transición obliga a todos a separar los programas entre “legacy” y “portados o al menos revisados”, lo que es muy doloroso.
Incluso cambiarlo por un tipo más grande con semántica similar puede romper cosas. Un ejemplo simple es el padding de estructuras, y también hay muchos casos de uso donde se convierte un puntero a entero y luego de vuelta; si cambia la representación interna, inevitablemente puede romperse. Que eso sea una buena práctica es otro tema, pero no es algo raro. El punto clave es la compatibilidad ABI.
off_tde 32 bits con otra compilada conoff_tde 64 bits, y el comportamiento resultante puede ser muy impredecible.En la estructura de ejemplo, se decía que el offset de
cera 8 contime_tde 32 bits y 12 con el tipo de 64 bits, pero en realidad me parece que debería ser 16. Comobdebe estar alineado a 64 bits, tendría que haber padding entreayb. De hecho, eso refuerza aún más el argumento del autor.Viendo todo esto, la extraña representación del tiempo de Windows —contar en unidades de 100 ns con 64 bits desde el 1 de enero de 1601 a las 00:00 GMT, considerado según el calendario gregoriano— también tiene una pequeña ventaja. La resolución es excelente y seguirá funcionando incluso hasta que se conquiste toda la galaxia.