<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Linux Sin Humo (Publicaciones sobre lastlog)</title><link>https://sergiobelkin.com/</link><description></description><atom:link href="https://sergiobelkin.com/categories/lastlog.xml" rel="self" type="application/rss+xml"></atom:link><language>es</language><copyright>Contents © 2026 &lt;a href="mailto:sebelk@gmail.com"&gt;sebelk&lt;/a&gt; 
&lt;a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/"&gt;
&lt;img alt="Creative Commons License BY-NC-SA"
style="border-width:0; margin-bottom:12px;"
src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png"&gt;&lt;/a&gt;
</copyright><lastBuildDate>Sun, 26 Jul 2026 00:22:54 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Una máquina de 64 bits no es inmune al 2038</title><link>https://sergiobelkin.com/posts/lastlog-y2038/</link><dc:creator>sebelk</dc:creator><description>&lt;figure&gt;&lt;img src="https://sergiobelkin.com/images/lastlog-y2038-bits.png"&gt;&lt;/figure&gt; &lt;p&gt;Las computadoras no guardan la fecha como la escribimos nosotros: guardan un número, y cada familia de sistemas eligió desde cuándo contarlo. En los sistemas Unix —Linux entre ellos— ese número es la cantidad de segundos transcurridos desde el 1 de enero de 1970. Ocupa un lugar de tamaño fijo, y cuando el lugar es chico, se llena. El lugar que se usó durante décadas se llena el 19 de enero de 2038. La mayoría de las máquinas de hoy ya no tienen ese límite; algunos archivos que esas mismas máquinas siguen escribiendo, sí.&lt;/p&gt;
&lt;p&gt;Lo que sigue está medido, no citado: cada cifra sale de correr el programa en la máquina que se nombra —una Fedora 44 y las tres versiones de RHEL vigentes—. Una de esas mediciones muestra algo que la documentación no dice: en la familia RHEL, el número de versión de glibc no alcanza para saber en qué situación está un sistema, y aplicarlo como criterio da la respuesta equivocada en la versión más nueva.&lt;/p&gt;
&lt;p&gt;Para lo principal —saber si tus máquinas están afectadas y por qué el número de versión no te lo contesta— alcanza con moverse en una terminal y leer la salida de un comando; eso está en &lt;em&gt;Dónde está parada tu máquina&lt;/em&gt; y en el cierre. Los tramos con código en C y aritmética de enteros explican el mecanismo: dan el fundamento, no la conclusión, y quien no los recorra llega igual al final.&lt;/p&gt;
&lt;p&gt;Ese número se guarda en un tipo de dato llamado &lt;code&gt;time_t&lt;/code&gt;, y de cuántos bits tenga depende hasta qué fecha puede seguir contando. Un &lt;code&gt;time_t&lt;/code&gt; de 32 bits con signo llega hasta enero de 2038 y ahí se le termina la cuenta. Ese es el problema del año 2038.&lt;/p&gt;
&lt;p&gt;Que una máquina sea "de 64 bits" es una etiqueta que se repite bastante más de lo que se explica. Lo que mide 64 bits son los registros del procesador —los casilleros internos donde hace sus cuentas— y las direcciones con las que ubica cada byte de la memoria: es el tamaño del número que puede manejar de un saque. En Linux sobre esas máquinas, los tipos de dato que representan direcciones, tamaños y tiempos pasaron a medir 64 bits, y &lt;code&gt;time_t&lt;/code&gt; es uno de ellos.&lt;/p&gt;
&lt;p&gt;Por eso en un servidor x86-64 —cualquiera con procesador Intel o AMD de las últimas dos décadas— &lt;code&gt;time_t&lt;/code&gt; mide 8 bytes, o sea 64 bits, y con ese ancho la cuenta no se agota en ningún plazo que nos importe. De ahí se deduce que la máquina está a salvo.&lt;/p&gt;
&lt;p&gt;Pero ese ancho es una propiedad del procesador y de los programas que se compilan para él. Un archivo que ya está escrito no participa de eso. La deducción vale para lo que el sistema calcula mientras corre, y no vale para lo que queda guardado en disco. Un archivo no guarda un &lt;code&gt;time_t&lt;/code&gt;: guarda una cantidad fija de bytes, en un orden fijo, que se decidió cuando se diseñó ese formato. Compilar el sistema para 64 bits no cambia esa decisión, porque agrandar un campo volvería ilegible todo lo que ya está escrito. Y &lt;code&gt;/var/log/lastlog&lt;/code&gt;, donde queda registrado el último inicio de sesión de cada usuario, es uno de esos formatos.&lt;/p&gt;
&lt;p&gt;Si compilás esto en cualquier Fedora o RHEL:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;lastlog.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;lastlog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"registro: %zu bytes | ll_time: %zu bytes, %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"con signo"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"sin signo"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No abre &lt;code&gt;/var/log/lastlog&lt;/code&gt; ni necesita privilegios: lo único que hace es preguntarle al compilador qué forma tiene la estructura en esa máquina. &lt;code&gt;sizeof&lt;/code&gt; se resuelve al compilar, así que las dos primeras cifras son el tamaño del registro y el del campo de la hora. La tercera sale de una prueba sencilla: se le asigna &lt;code&gt;-1&lt;/code&gt; al campo y se pregunta si quedó negativo. Si el campo tiene signo, &lt;code&gt;-1&lt;/code&gt; sigue valiendo &lt;code&gt;-1&lt;/code&gt;; si no lo tiene, ese mismo patrón de bits se lee como 4.294.967.295 y la comparación da falso.&lt;/p&gt;
&lt;p&gt;En una Fedora 44 con glibc 2.43 imprime:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;registro&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;292&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;signo&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Tres datos en una sola línea. El registro mide 292 bytes: es el tamaño fijo del casillero que le corresponde a cada usuario dentro del archivo. De esos 292, solamente cuatro guardan la fecha del último acceso — cuatro bytes, en una máquina donde el sistema cuenta el tiempo con ocho. Y esos cuatro se están leyendo como un número que no admite valores negativos, que es el detalle que decide si la cuenta se agota en 2038 o llega hasta 2106.&lt;/p&gt;
&lt;h3 id="por-que-el-campo-quedo-en-32-bits"&gt;Por qué el campo quedó en 32 bits&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;struct lastlog&lt;/code&gt; la define glibc, no &lt;code&gt;shadow&lt;/code&gt; —el paquete de las herramientas de cuentas de usuario, &lt;code&gt;useradd&lt;/code&gt;, &lt;code&gt;chage&lt;/code&gt;, &lt;code&gt;passwd&lt;/code&gt;, y del que en RHEL sale también el comando &lt;code&gt;lastlog&lt;/code&gt;—. glibc es la biblioteca estándar de C del sistema: además de proveer las funciones con las que los programas le hablan al kernel, define las estructuras de datos que esos programas comparten entre sí. Por eso puede fijar el formato de un archivo que ella misma no escribe.&lt;/p&gt;
&lt;p&gt;Su declaración actual tiene una condición:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#if __WORDSIZE_TIME64_COMPAT32&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;__uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;__time_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#endif&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;__WORDSIZE_TIME64_COMPAT32&lt;/code&gt; vale 1 en x86-64 y 0 en aarch64. Lo que expresa esa macro es que en x86-64 conviven programas de 64 bits con programas compilados para 32, y que unos y otros leen los mismos archivos del sistema. Si &lt;code&gt;struct lastlog&lt;/code&gt; tuviera un &lt;code&gt;ll_time&lt;/code&gt; de 8 bytes al compilarse a 64 bits y de 4 al compilarse a 32, cada uno vería una grilla distinta sobre el mismo &lt;code&gt;/var/log/lastlog&lt;/code&gt;: donde el programa de 64 bits guardó la fecha, el de 32 leería la mitad de esa fecha, y a continuación tomaría la otra mitad como si fuera el principio del nombre de la terminal. glibc evita esa contradicción fijando el campo en 32 bits para las arquitecturas que arrastran esa compatibilidad, aunque el &lt;code&gt;time_t&lt;/code&gt; de la plataforma sea de 64.&lt;/p&gt;
&lt;p&gt;Es una decisión sobre el &lt;strong&gt;formato en disco&lt;/strong&gt;, no sobre la aritmética del sistema. Por eso ser de 64 bits no alcanza. En aarch64, que nunca tuvo esa restricción, el mismo campo mide 8 bytes y el registro mide 296 en vez de 292 — con las consecuencias de portabilidad que ya vimos en &lt;a href="https://sergiobelkin.com/posts/lastlog-particularidades/"&gt;las particularidades de lastlog&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="el-problema-tal-como-era"&gt;El problema, tal como era&lt;/h3&gt;
&lt;p&gt;Hasta hace poco ese campo de 4 bytes era &lt;strong&gt;con signo&lt;/strong&gt;, y de ahí sale la fecha famosa.&lt;/p&gt;
&lt;p&gt;Con 32 bits se pueden anotar 4.294.967.296 valores distintos. Poder representar números negativos cuesta la mitad de ese total: se va en las fechas anteriores a 1970, y el valor positivo más grande que entra pasa a ser 2.147.483.647. Ese número no es una abstracción, son segundos, y 2.147.483.647 segundos son unos 68 años. Contados desde el 1 de enero de 1970, la cuenta llega hasta:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;2038&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;19&lt;/span&gt;&lt;span class="n"&gt;T03&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;07&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Un segundo después desborda al valor negativo más chico. Y la ruta de lectura de &lt;code&gt;shadow&lt;/code&gt; hace exactamente esto:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;time_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Cuando ese valor de 32 bits pasa a una variable de 64, el compilador se ocupa de que siga significando lo mismo: si era negativo, rellena los bits nuevos de manera que siga siendo el mismo número negativo. Se llama &lt;strong&gt;extensión de signo&lt;/strong&gt;, y acá tiene una consecuencia visible. El registro de un login ocurrido en 2038 no se mostraría como una fecha rara del futuro: se mostraría como&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;1901&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;12&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;13&lt;/span&gt;&lt;span class="n"&gt;T20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;45&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;52&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;La escritura tiene la falla simétrica, y llega por dos caminos. En el propio comando, cuando &lt;code&gt;lastlog -S&lt;/code&gt; fija una entrada a mano, es una sola línea: &lt;code&gt;ll.ll_time = NOW;&lt;/code&gt;, donde &lt;code&gt;NOW&lt;/code&gt; es &lt;code&gt;time((time_t *) 0)&lt;/code&gt;. En cada inicio de sesión el que escribe es el módulo &lt;code&gt;pam_lastlog&lt;/code&gt;, que hace lo mismo en dos pasos: &lt;code&gt;time(&amp;amp;ll_time)&lt;/code&gt; sobre un &lt;code&gt;time_t&lt;/code&gt; de 64 bits y &lt;code&gt;last_login.ll_time = ll_time;&lt;/code&gt; sobre el campo de 32. En ambos casos el valor se trunca al guardarse.&lt;/p&gt;
&lt;p&gt;Ese es el problema del 2038 en su forma concreta: no una abstracción sobre "sistemas viejos", sino un campo de un registro de 292 bytes que un RHEL 8 o un RHEL 9 escriben hoy en cada inicio de sesión.&lt;/p&gt;
&lt;h3 id="lo-que-hizo-glibc"&gt;Lo que hizo glibc&lt;/h3&gt;
&lt;p&gt;En la versión 2.40, publicada el 22 de julio de 2024, glibc cambió el tipo de ese campo. De las notas de la versión:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Architectures which use a 32-bit seconds-since-epoch field in struct lastlog, struct utmp, struct utmpx (such as i386, powerpc64le, rv32, rv64, x86-64) switched from a signed to an unsigned type for that field. This allows these fields to store timestamps beyond the year 2038, until the year 2106. Please note that applications are still expected to migrate off the interfaces declared in &lt;code&gt;&amp;lt;utmp.h&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;utmpx.h&amp;gt;&lt;/code&gt; (except for login_tty) due to locking and session management problems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Los mismos cuatro bytes, si ya no tienen que reservar la mitad de su capacidad para las fechas anteriores a 1970, llegan al doble: 4.294.967.295 segundos, otros 68 años. La cuenta se agota recién el:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;2106&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;02&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;07&lt;/span&gt;&lt;span class="n"&gt;T06&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;15&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Y la extensión ahora es de ceros, no de signo: &lt;code&gt;ll_time = ll.ll_time;&lt;/code&gt; sobre un &lt;code&gt;__uint32_t&lt;/code&gt; produce un &lt;code&gt;time_t&lt;/code&gt; positivo. El login de 2038 se lee como 2038.&lt;/p&gt;
&lt;p&gt;&lt;a class="image-reference" href="https://sergiobelkin.com/images/lastlog-y2038-bits.svg"&gt;&lt;img src="https://sergiobelkin.com/images/lastlog-y2038-bits.svg" alt="El campo ll_time de 32 bits con el bit de más peso en uno y el resto en cero, mostrado como cuatro bytes. Leído con signo, como lo declaraba glibc hasta la versión 2.40, ese patrón vale menos 2.147.483.648 segundos: el 13 de diciembre de 1901. Leído sin signo, como lo declara glibc desde 2.40, vale 2.147.483.648 segundos: el 19 de enero de 2038. Abajo, una línea de tiempo muestra que el tramo de 1970 a 2038 y el que va de 2038 a 2106 miden lo mismo, 68 años cada uno." style="max-width: 100%; height: auto;"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Es la razón por la que el programa del principio imprime &lt;code&gt;sin signo&lt;/code&gt;. Donde el campo siga declarado como &lt;code&gt;int32_t&lt;/code&gt; imprimirá &lt;code&gt;con signo&lt;/code&gt;, y eso —y no el número de versión de glibc, por los motivos que veremos al final— es lo que hay que medir antes de afirmar en qué situación está una máquina concreta.&lt;/p&gt;
&lt;h3 id="lo-que-ese-cambio-gana-y-lo-que-no"&gt;Lo que ese cambio gana, y lo que no&lt;/h3&gt;
&lt;p&gt;El cambio se lee de dos maneras equivocadas: "el problema está resuelto" o "fue un parche inútil". No es ninguna de las dos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gana 68 años sin tocar el formato.&lt;/strong&gt; El registro sigue midiendo 292 bytes, los offsets siguen siendo &lt;code&gt;UID × 292&lt;/code&gt;, y un archivo escrito antes del cambio se sigue leyendo igual. De los 32 bits, hay uno —el de más peso— que en la lectura con signo es el que indica si el número es negativo. Todas las marcas anteriores a 2038 lo tienen en cero, así que esos mismos bytes significan lo mismo con o sin signo. Precisamente por eso el cambio era viable: no rompe nada de lo que ya está escrito.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No ensancha nada.&lt;/strong&gt; El campo sigue teniendo 32 bits. La pared se corrió, no desapareció.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No es una maniobra exclusiva de glibc.&lt;/strong&gt; MariaDB llegó a la misma pared y salió por la misma puerta: desde su versión 11.5, el tipo &lt;code&gt;TIMESTAMP&lt;/code&gt; pasó de 2038 a 2106 reinterpretando como sin signo el mismo campo de cuatro bytes, sin tocar el formato en disco —&lt;em&gt;"storage of timestamp is not affected by the change"&lt;/em&gt;, dice el reporte— y solo en plataformas de 64 bits. Dos proyectos sin relación entre sí, con la misma restricción y la misma salida, hasta la misma fecha.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cambia el significado de un valor sin que el archivo lo declare.&lt;/strong&gt; Un registro con ese bit en uno significaba 1901 antes del cambio y significa 2038 o más después. En la práctica es inocuo, porque nadie tiene logins anteriores a 1970. Pero el archivo no tiene número de versión ni campo de tipo: nada adentro dice bajo qué interpretación fue escrito. Lo que decide es la glibc del binario que lo lee.&lt;/p&gt;
&lt;h3 id="por-que-no-bastaba-con-agrandar-el-campo"&gt;Por qué no bastaba con agrandar el campo&lt;/h3&gt;
&lt;p&gt;Si el campo pasara de 4 a 8 bytes, &lt;code&gt;sizeof(struct lastlog)&lt;/code&gt; pasaría de 292 a 296. Pero el archivo no guarda el nombre del usuario: la identidad de cada registro &lt;strong&gt;es su posición&lt;/strong&gt;, calculada como &lt;code&gt;UID × sizeof(struct lastlog)&lt;/code&gt;. Cambiar el tamaño del registro cambia todos los offsets a la vez. El archivo entero deja de ser legible, y no hay ningún campo adentro que permita detectar con qué tamaño fue escrito.&lt;/p&gt;
&lt;p&gt;Un formato que indexa por posición no puede evolucionar su registro. Esa rigidez es el precio de poder saltar directo al registro de un usuario sin recorrer el archivo, y es lo que convierte al ensanchamiento del campo en un callejón sin salida. Pasar a &lt;code&gt;unsigned&lt;/code&gt; fue la única maniobra posible sin romper compatibilidad, y funciona porque no cambia ni un byte del formato.&lt;/p&gt;
&lt;h3 id="el-arreglo-de-verdad-lastlog2"&gt;El arreglo de verdad: lastlog2&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;lastlog2&lt;/code&gt; es un proyecto de Thorsten Kukuk integrado en &lt;strong&gt;util-linux 2.40&lt;/strong&gt;. Abandona el archivo indexado por posición y guarda los datos en una base SQLite3 en &lt;code&gt;/var/lib/lastlog/lastlog2.db&lt;/code&gt;, con una tabla cuya clave es el &lt;strong&gt;nombre de usuario&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Lastlog2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;Time&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;TTY&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;RemoteHost&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Escribir un login es un &lt;code&gt;REPLACE INTO&lt;/code&gt; por nombre; leer es un &lt;code&gt;SELECT&lt;/code&gt; por nombre. La marca de tiempo es un entero de 64 bits, y el tamaño de la base depende de la cantidad de usuarios con registro, no del UID más alto. Los datos los recolecta un módulo PAM, &lt;code&gt;pam_lastlog2&lt;/code&gt;, que reemplaza a &lt;code&gt;pam_lastlog&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Cada diferencia cierra uno de los problemas de la trilogía: la clave por nombre elimina las colisiones de UID; la base densa elimina el &lt;a href="https://sergiobelkin.com/posts/sparse-files-linux/"&gt;archivo disperso&lt;/a&gt; y su tamaño aparente descomunal; el entero de 64 bits elimina la fecha límite. Y como la identidad ya no es la posición, el esquema puede crecer: el campo &lt;code&gt;Service&lt;/code&gt; no existía en &lt;code&gt;struct lastlog&lt;/code&gt; y no podría haberse agregado.&lt;/p&gt;
&lt;p&gt;Una aclaración cronológica: la justificación que da el propio proyecto —&lt;em&gt;"the standard &lt;code&gt;/var/log/lastlog&lt;/code&gt; implementation using &lt;code&gt;lastlog.h&lt;/code&gt; from glibc uses a 32bit &lt;code&gt;time_t&lt;/code&gt;"&lt;/em&gt;— se escribió cuando el campo todavía era con signo. El cambio de glibc corrió la fecha, no eliminó el motivo.&lt;/p&gt;
&lt;p&gt;Fedora migró a &lt;code&gt;lastlog2&lt;/code&gt; como default del sistema en su versión 43. En una Fedora 44 el binario &lt;code&gt;lastlog&lt;/code&gt; ya no existe, &lt;code&gt;/var/log/lastlog&lt;/code&gt; queda como un archivo vacío, y &lt;code&gt;rpm -ql shadow-utils | grep -i lastlog&lt;/code&gt; no devuelve nada. glibc, en cambio, sigue instalando &lt;code&gt;&amp;lt;lastlog.h&amp;gt;&lt;/code&gt;: la estructura sobrevive al programa.&lt;/p&gt;
&lt;h3 id="utmp-y-wtmp-corren-la-misma-suerte"&gt;utmp y wtmp corren la misma suerte&lt;/h3&gt;
&lt;p&gt;El cambio de glibc 2.40 alcanzó también a &lt;code&gt;struct utmp&lt;/code&gt; y &lt;code&gt;struct utmpx&lt;/code&gt;, que sostienen &lt;code&gt;/run/utmp&lt;/code&gt;, &lt;code&gt;/var/log/wtmp&lt;/code&gt; y &lt;code&gt;/var/log/btmp&lt;/code&gt;. Se verifica con el mismo programa, cambiando &lt;code&gt;&amp;lt;lastlog.h&amp;gt;&lt;/code&gt; por &lt;code&gt;&amp;lt;utmp.h&amp;gt;&lt;/code&gt;, la estructura por &lt;code&gt;struct utmp&lt;/code&gt; y el campo por &lt;code&gt;ut_tv.tv_sec&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sizeof(struct utmp) = 384 | ut_tv.tv_sec = 4 bytes, sin signo
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;La diferencia es que ahí no hay un &lt;code&gt;lastlog2&lt;/code&gt; esperando. La dirección de upstream, sostenida por el mismo autor, es dejar de escribir &lt;code&gt;utmp&lt;/code&gt; y consultar a &lt;code&gt;systemd-logind&lt;/code&gt; a través de las funciones &lt;code&gt;sd_*()&lt;/code&gt; de &lt;code&gt;libsystemd&lt;/code&gt;. Es un reemplazo más ambicioso que cambiar un backend de almacenamiento, y por eso avanza más despacio.&lt;/p&gt;
&lt;h3 id="donde-esta-parada-tu-maquina"&gt;Dónde está parada tu máquina&lt;/h3&gt;
&lt;p&gt;Tres comprobaciones, ninguna destructiva.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La primera&lt;/strong&gt; es el programa del principio, que lee la estructura tal como la ve el compilador de esa máquina. Si no hay compilador a mano, la declaración se puede mirar directamente en el archivo donde glibc la define, que viene en el paquete &lt;code&gt;glibc-devel&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;grep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;A6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'struct lastlog'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;include&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;bits&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;utmp&lt;/span&gt;.&lt;span class="nv"&gt;h&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;La primera rama es la que se compila en x86-64 y en las demás arquitecturas que arrastran compatibilidad con programas de 32 bits: &lt;code&gt;int32_t&lt;/code&gt; significa 2038, &lt;code&gt;__uint32_t&lt;/code&gt; significa 2106. La segunda rama, &lt;code&gt;__time_t&lt;/code&gt;, es la de aarch64, donde el campo mide 8 bytes y el problema no existe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La segunda&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lastlog&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lastlog2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Si existe, el sistema ya está en el esquema SQLite y el problema no aplica.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La tercera&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lastlog&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Si informa un tamaño aparente grande y una ocupación mínima, el archivo indexado por UID sigue vivo, y con él tanto la fecha límite de su campo de 32 bits como todo lo que ya sabemos sobre no rotarlo y no meterlo en un backup a nivel de archivo.&lt;/p&gt;
&lt;p&gt;Lo que conviene no hacer es deducirlo del número de versión. &lt;code&gt;rpm -q glibc&lt;/code&gt; parece suficiente, porque el cambio es de 2.40, pero las distribuciones retroportan. En la familia de RHEL el número de versión no alcanza para deducir la respuesta:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sistema&lt;/th&gt;
&lt;th&gt;glibc&lt;/th&gt;
&lt;th&gt;Campo&lt;/th&gt;
&lt;th&gt;Límite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RHEL 8.10&lt;/td&gt;
&lt;td&gt;2.28&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int32_t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2038&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RHEL 9.8&lt;/td&gt;
&lt;td&gt;2.34&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int32_t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2038&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RHEL 10.2&lt;/td&gt;
&lt;td&gt;2.39&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__uint32_t&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2106&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;RHEL 10 tiene el arreglo sin haber llegado a 2.40: Red Hat lo retroportó a su glibc 2.39 en mayo de 2024, en la entrada &lt;code&gt;2.39-12&lt;/code&gt; del changelog del paquete. Quien aplique el criterio de la versión sobre RHEL 10 va a concluir que está expuesto a 2038, y no lo está. Las tres filas salen de correr el programa dentro de &lt;code&gt;ubi8&lt;/code&gt;, &lt;code&gt;ubi9&lt;/code&gt; y &lt;code&gt;ubi10&lt;/code&gt;, que llevan la glibc de cada RHEL.&lt;/p&gt;
&lt;h3 id="lo-mismo-en-una-maquina-de-32-bits"&gt;Lo mismo en una máquina de 32 bits&lt;/h3&gt;
&lt;p&gt;Nada de esto es propio de RHEL. En una Debian 12 sobre i386 el mismo &lt;code&gt;grep&lt;/code&gt; devuelve &lt;code&gt;int32_t&lt;/code&gt;. Y ahí el campo de cuatro bytes no está solo: en una máquina de 32 bits todos los programas guardan el tiempo en cuatro bytes, no únicamente los que tocan &lt;code&gt;lastlog&lt;/code&gt;. Con el reloj puesto más allá de 2038, esa máquina ni siquiera termina de arrancar: el programa que le entrega el control al sistema definitivo pide los datos de un directorio, la fecha no entra en el campo, y el arranque se queda en el shell de emergencia.&lt;/p&gt;
&lt;p&gt;&lt;a class="image-reference" href="https://sergiobelkin.com/images/debian-i386-2038-no-arranca.png"&gt;&lt;img src="https://sergiobelkin.com/images/debian-i386-2038-no-arranca.png" alt="Consola de una máquina virtual con Debian 12 sobre i386, arrancando con el reloj puesto más allá de 2038. El sistema de archivos se monta sin problemas —el propio log dice «/dev/vda1: clean, 37655/1248480 files»— pero el arranque repite seis veces «run-init: can't stat '.': Value too large for defined data type», informa «Target filesystem doesn't have requested /sbin/init» y «No init found. Try passing init= bootarg», y termina cayendo al shell de emergencia de BusyBox con el prompt (initramfs)." style="max-width: 100%; height: auto;"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;El disco está sano y el kernel también: el propio arranque informa que el sistema de archivos está limpio, con sus 37.655 archivos en su lugar. Lo único que pasó es que una fecha dejó de entrar en un campo de cuatro bytes.&lt;/p&gt;
&lt;p&gt;Y esa máquina va a seguir así: cuando Debian pasó sus arquitecturas de 32 bits a tiempos de 64 bits, en la versión 13, &lt;strong&gt;dejó a i386 deliberadamente afuera&lt;/strong&gt;. La razón es que i386 se mantiene para correr binarios viejos, y cambiar el tamaño de los tipos rompería justamente esa compatibilidad. De todos modos, la diferencia con &lt;code&gt;lastlog&lt;/code&gt; se sostiene: este es un problema que se arregla recompilando los programas —Debian lo hizo para el resto de sus arquitecturas de 32 bits— y el de &lt;code&gt;lastlog&lt;/code&gt; no, porque recompilar no cambia lo que ya está escrito en disco.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lastlog&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lastlog2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Si existe, el sistema ya está en el esquema SQLite y el problema no aplica.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lastlog&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Si informa un tamaño aparente grande y una ocupación mínima, el archivo indexado por UID sigue vivo, y con él tanto la fecha límite de su campo de 32 bits como todo lo que ya sabemos sobre no rotarlo y no meterlo en un backup a nivel de archivo.&lt;/p&gt;
&lt;h3 id="para-que-se-corrio-la-pared"&gt;Para qué se corrió la pared&lt;/h3&gt;
&lt;p&gt;Los 68 años que agregó el cambio no son para llegar a 2106. Nadie va a estar leyendo &lt;code&gt;/var/log/lastlog&lt;/code&gt; en 2106: el reemplazo ya existe, ya es el default en Fedora, y no hay un tercer truco disponible. Cuatro bytes sin signo llegan a 4.294.967.295 y ahí se termina — no queda nada que reinterpretar. Correr la época (&lt;em&gt;epoch&lt;/em&gt;), empezar a contar desde 2038 en vez de desde 1970, tampoco sirve: a diferencia del cambio de signo, haría que cada valor ya escrito significara otra cosa, y el archivo no tiene adentro ningún campo que diga bajo qué época fue guardado.&lt;/p&gt;
&lt;p&gt;Los 68 años son para las máquinas que ya están andando. Una RHEL 8 instalada este año va a seguir en producción bien entrados los 2030, con su campo con signo y su fecha límite en 2038 — que está a doce años, no a ochenta. RHEL 9 está en la misma situación. Para esas máquinas el problema no es lejano: es la vida útil del servidor que alguien está montando ahora.&lt;/p&gt;
&lt;p&gt;Eso es lo que hace el cambio de glibc. No arregla el formato, que sigue sin poder crecer. Le da al reemplazo el tiempo de propagarse al ritmo al que se reemplazan los servidores, en lugar del ritmo de una urgencia.&lt;/p&gt;
&lt;h3 id="fuentes-y-mas-recursos"&gt;Fuentes y más recursos&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://lists.gnu.org/archive/html/info-gnu/2024-07/msg00013.html"&gt;glibc 2.40 — anuncio de la versión&lt;/a&gt; — el párrafo que documenta el cambio de &lt;code&gt;signed&lt;/code&gt; a &lt;code&gt;unsigned&lt;/code&gt; en el campo de época de &lt;code&gt;struct lastlog&lt;/code&gt;, &lt;code&gt;utmp&lt;/code&gt; y &lt;code&gt;utmpx&lt;/code&gt;, y el corrimiento del límite a 2106.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.thkukuk.de/blog/Y2038_glibc_utmp_64bit/"&gt;Y2038, glibc and utmp/utmpx on 64bit architectures&lt;/a&gt; — Thorsten Kukuk explica por qué una máquina de 64 bits no es inmune: el &lt;code&gt;time_t&lt;/code&gt; de 32 bits que sobrevive en los formatos de utmp y lastlog.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/redhat/centos-stream/rpms/glibc/-/blob/c10s/glibc.spec"&gt;glibc.spec de CentOS Stream 10&lt;/a&gt; — el changelog del paquete, donde consta el retroporte del cambio a la glibc 2.39 de RHEL 10: &lt;em&gt;"Use unsigned types in &lt;code&gt;&amp;lt;utmp.h&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;utmpx.h&amp;gt;&lt;/code&gt;"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jira.mariadb.org/browse/MDEV-32188"&gt;MDEV-32188 — make TIMESTAMP use whole 32-bit unsigned range&lt;/a&gt; — el mismo movimiento en MariaDB: de 32 bits con signo a 32 bits sin signo, de 2038 a 2106, sin cambiar el formato de almacenamiento. Corregido en 11.5.1.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/thkukuk/lastlog2"&gt;Proyecto lastlog2&lt;/a&gt; — el reemplazo Y2038-safe con backend SQLite3: &lt;code&gt;liblastlog2&lt;/code&gt;, la CLI y &lt;code&gt;pam_lastlog2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/pub/linux/utils/util-linux/v2.40/v2.40-ReleaseNotes"&gt;util-linux 2.40 — Release Notes&lt;/a&gt; — la integración de &lt;code&gt;lastlog2&lt;/code&gt; y &lt;code&gt;pam_lastlog2&lt;/code&gt; en util-linux.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fedoraproject.org/wiki/Changes/Migrate_to_lastlog2"&gt;Fedora Change: Migrate to lastlog2&lt;/a&gt; — la adopción de &lt;code&gt;lastlog2&lt;/code&gt; como default del sistema, desde Fedora 43.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="la-serie"&gt;La serie&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parte&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;De qué trata&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://sergiobelkin.com/posts/sparse-files-linux/"&gt;Archivos sparse en Linux&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Los agujeros: cómo un archivo declara un tamaño y ocupa otro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://sergiobelkin.com/posts/lastlog-particularidades/"&gt;Particularidades de lastlog&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;El UID como posición dentro del archivo, y todo lo que se desprende de esa decisión&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Una máquina de 64 bits no es inmune al 2038&lt;/strong&gt; &lt;em&gt;(estás acá)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;El campo de fecha de 32 bits, lo que hizo glibc y el reemplazo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description><category>glibc</category><category>lastlog</category><category>sysadmin</category><category>y2038</category><guid>https://sergiobelkin.com/posts/lastlog-y2038/</guid><pubDate>Sat, 25 Jul 2026 21:22:37 GMT</pubDate></item><item><title>Particularidades de lastlog</title><link>https://sergiobelkin.com/posts/lastlog-particularidades/</link><dc:creator>sebelk</dc:creator><description>&lt;figure&gt;&lt;img src="https://sergiobelkin.com/images/lastlog-mapa.png"&gt;&lt;/figure&gt; &lt;p&gt;En un RHEL 8 recién instalado, &lt;code&gt;/var/log/lastlog&lt;/code&gt; no llama la atención. En el mismo RHEL 8 unido a un dominio por SSSD, después de que un solo usuario del dominio inicie sesión, &lt;code&gt;ls -l&lt;/code&gt; puede informar un archivo de cientos de gigabytes. &lt;code&gt;du -h&lt;/code&gt; sobre ese mismo archivo devuelve unos pocos kilobytes.&lt;/p&gt;
&lt;p&gt;El archivo es &lt;a href="https://sergiobelkin.com/posts/sparse-files-linux/"&gt;sparse&lt;/a&gt;, y eso explica la diferencia entre los dos números. Lo que explica &lt;em&gt;por qué&lt;/em&gt; es sparse es una sola línea de código.&lt;/p&gt;
&lt;h3 id="el-uid-es-la-direccion"&gt;El UID es la dirección&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/var/log/lastlog&lt;/code&gt; guarda el último inicio de sesión de cada usuario. No tiene índice, ni cabecera, ni claves, ni base de datos. La posición del registro dentro del archivo &lt;strong&gt;es&lt;/strong&gt; el UID. En &lt;code&gt;shadow-utils&lt;/code&gt; el cálculo aparece idéntico en la ruta de lectura y en la de escritura:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;off_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pw_uid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;El archivo se trata como un arreglo en disco cuyo índice es el UID, exactamente igual que &lt;code&gt;array[i]&lt;/code&gt; vive en &lt;code&gt;base + i * sizeof(elemento)&lt;/code&gt;. Para leer el último login de cualquier usuario se hace un &lt;code&gt;fseeko&lt;/code&gt; a ese offset y se lee un registro. Acceso directo, sin parsear nada, sin mantener una estructura auxiliar.&lt;/p&gt;
&lt;p&gt;El &lt;code&gt;(off_t)&lt;/code&gt; merece atención. La conversión está &lt;strong&gt;antes&lt;/strong&gt; de la multiplicación, y no es un detalle cosmético: fuerza a que la operación se haga en 64 bits. Si se multiplicara primero en 32 bits, un UID grande desbordaría antes de convertir. Un UID de 20 000 000 por 292 bytes da unos 5840 millones, que no entra en 32 bits. El offset resultante apuntaría a cualquier parte.&lt;/p&gt;
&lt;h3 id="el-registro-y-por-que-mide-lo-que-mide"&gt;El registro, y por qué mide lo que mide&lt;/h3&gt;
&lt;p&gt;La &lt;code&gt;struct lastlog&lt;/code&gt; no la define &lt;code&gt;shadow&lt;/code&gt;: viene de glibc —se declara en &lt;code&gt;&amp;lt;bits/utmp.h&amp;gt;&lt;/code&gt;, que un programa obtiene incluyendo &lt;code&gt;&amp;lt;lastlog.h&amp;gt;&lt;/code&gt;—. Son tres campos:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Campo&lt;/th&gt;
&lt;th&gt;Tamaño&lt;/th&gt;
&lt;th&gt;Contenido&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ll_time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 bytes&lt;/td&gt;
&lt;td&gt;segundos desde epoch del último login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ll_line&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32 bytes&lt;/td&gt;
&lt;td&gt;terminal: &lt;code&gt;pts/0&lt;/code&gt;, &lt;code&gt;tty1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ll_host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;256 bytes&lt;/td&gt;
&lt;td&gt;host remoto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;292 bytes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Ese 292 no es universal, y ahí hay una particularidad que se pasa por alto. El campo &lt;code&gt;ll_time&lt;/code&gt; está definido bajo una condición:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#if __WORDSIZE_TIME64_COMPAT32&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;__uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;__time_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ll_time&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cp"&gt;#endif&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;En x86-64 esa macro vale 1, &lt;code&gt;ll_time&lt;/code&gt; ocupa 4 bytes y el registro mide 292. En aarch64 vale 0, &lt;code&gt;ll_time&lt;/code&gt; pasa a ser un &lt;code&gt;__time_t&lt;/code&gt; de 8 bytes, y el registro mide &lt;strong&gt;296&lt;/strong&gt;. Mismo archivo, mismo programa, distinta grilla.&lt;/p&gt;
&lt;p&gt;La consecuencia práctica es que &lt;code&gt;/var/log/lastlog&lt;/code&gt; no es portable entre arquitecturas. Copiado de un x86-64 a un aarch64, cada registro se lee corrido, y no hay número de versión ni campo de tipo que permita detectarlo. Es un volcado binario de una estructura de C, con la endianness y el padding de la máquina que lo escribió. Ese &lt;code&gt;ll_time&lt;/code&gt; condicional además arrastra el problema del año 2038.&lt;/p&gt;
&lt;h3 id="el-nombre-del-usuario-no-esta-guardado"&gt;El nombre del usuario no está guardado&lt;/h3&gt;
&lt;p&gt;Mirá de nuevo la tabla: hay tiempo, terminal y host. No hay nombre. La identidad del usuario no está &lt;em&gt;en&lt;/em&gt; el registro, está en la &lt;strong&gt;posición&lt;/strong&gt; del registro. Si el offset es &lt;code&gt;UID × 292&lt;/code&gt;, entonces &lt;code&gt;UID = offset / 292&lt;/code&gt;, y eso es todo lo que el archivo sabe sobre quién es quién.&lt;/p&gt;
&lt;p&gt;De ahí sale un comportamiento que sorprende cuando aparece. Si dos entradas de &lt;code&gt;/etc/passwd&lt;/code&gt; comparten UID:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;::/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="sr"&gt;/alice:/bin/&lt;/span&gt;&lt;span class="n"&gt;bash&lt;/span&gt;
&lt;span class="n"&gt;bob&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;::/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="sr"&gt;/bob:/bin/&lt;/span&gt;&lt;span class="n"&gt;bash&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;las dos cuentas comparten físicamente el mismo registro. &lt;code&gt;lastlog&lt;/code&gt; recorre &lt;code&gt;/etc/passwd&lt;/code&gt; con &lt;code&gt;getpwent()&lt;/code&gt; y procesa cada entrada por separado, así que imprime &lt;strong&gt;dos filas&lt;/strong&gt;, con nombres distintos y exactamente la misma fecha, la misma terminal y el mismo host. Si entró &lt;code&gt;alice&lt;/code&gt;, la salida muestra a &lt;code&gt;bob&lt;/code&gt; con esa fecha.&lt;/p&gt;
&lt;p&gt;En escritura es peor de razonar. &lt;code&gt;lastlog -S -u bob&lt;/code&gt; hace &lt;code&gt;getpwnam("bob")&lt;/code&gt; para obtener el UID, y desde ahí todo el trabajo se hace por UID. Lo que se actualiza es el registro del UID 1000, indistinguible del de &lt;code&gt;alice&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;No es un defecto de &lt;code&gt;lastlog&lt;/code&gt;. Es lo que significa tratar el UID como identidad, que es exactamente lo que hace el kernel: los permisos, la propiedad de los archivos y las credenciales de proceso van por UID. Los nombres son etiquetas en &lt;code&gt;/etc/passwd&lt;/code&gt;. Dos nombres con el mismo UID son el mismo usuario a todos los efectos, y &lt;code&gt;lastlog&lt;/code&gt; hereda esa ambigüedad en lugar de inventarse una propia.&lt;/p&gt;
&lt;p&gt;Un corolario del mismo diseño: como &lt;code&gt;lastlog&lt;/code&gt; recorre &lt;code&gt;passwd&lt;/code&gt; en vez de recorrer el archivo, los registros de usuarios ya borrados siguen físicamente en disco pero no se listan. Y si &lt;code&gt;nsswitch&lt;/code&gt; está respaldado por SSSD contra un directorio de decenas de miles de usuarios, ese &lt;code&gt;getpwent()&lt;/code&gt; enumera toda la base remota. El cuello de botella pasa a ser NSS, no el archivo.&lt;/p&gt;
&lt;h3 id="leer-es-seguro-escribir-es-lo-que-infla"&gt;Leer es seguro; escribir es lo que infla&lt;/h3&gt;
&lt;p&gt;Hay una asimetría en el código que determina cuándo el archivo crece.&lt;/p&gt;
&lt;p&gt;En &lt;strong&gt;lectura&lt;/strong&gt;, antes de posicionarse se comprueba que el registro entre dentro del archivo:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;statbuf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Si el UID cae más allá del final, no se lee nada: la estructura se llena de ceros con &lt;code&gt;memzero&lt;/code&gt; y se imprime &lt;code&gt;**Never logged in**&lt;/code&gt;. Consultar el &lt;code&gt;lastlog&lt;/code&gt; de un usuario con UID enorme &lt;strong&gt;no&lt;/strong&gt; agranda el archivo. Y como un agujero también se lee como ceros, el código trata igual al "usuario dentro de un agujero" y al "usuario fuera del archivo". Ambos son lo mismo: nadie inició sesión.&lt;/p&gt;
&lt;p&gt;En &lt;strong&gt;escritura&lt;/strong&gt; no hay ninguna comprobación de límites: se hace &lt;code&gt;fseeko&lt;/code&gt; al offset y se escribe. Un solo login —o un solo &lt;code&gt;lastlog -S -u &amp;lt;usuario&amp;gt;&lt;/code&gt;— con un UID alto extiende el archivo hasta ese offset y crea el agujero intermedio.&lt;/p&gt;
&lt;p&gt;Por eso el tamaño aparente del archivo no depende de cuántos usuarios tenés, sino del &lt;strong&gt;UID más alto que alguna vez haya escrito un registro&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="los-numeros-con-un-directorio-corporativo"&gt;Los números con un directorio corporativo&lt;/h3&gt;
&lt;p&gt;Con cuentas locales, los UID rondan el millar y el archivo es irrelevante. Con Active Directory o LDAP vía SSSD y mapeo algorítmico de SID a POSIX ID, el panorama cambia. Los defaults de SSSD ubican el rango de UID mapeados entre &lt;code&gt;ldap_idmap_range_min&lt;/code&gt; = &lt;code&gt;200000&lt;/code&gt; y &lt;code&gt;ldap_idmap_range_max&lt;/code&gt; = &lt;code&gt;2000200000&lt;/code&gt;, repartido en slices de &lt;code&gt;200000&lt;/code&gt;, y el slice que le toca a cada dominio se elige por hash de su SID. Es decir: el UID de un usuario de dominio puede caer en cualquier punto de ese rango.&lt;/p&gt;
&lt;p&gt;A 292 bytes por registro:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;UID que inició sesión&lt;/th&gt;
&lt;th&gt;Tamaño aparente&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 000&lt;/td&gt;
&lt;td&gt;285 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 000 000&lt;/td&gt;
&lt;td&gt;278 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 000 000&lt;/td&gt;
&lt;td&gt;27,2 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;145 000 000&lt;/td&gt;
&lt;td&gt;39,4 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 000 199 999 (tope del rango)&lt;/td&gt;
&lt;td&gt;543,9 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;a class="image-reference" href="https://sergiobelkin.com/images/lastlog-mapa.svg"&gt;&lt;img src="https://sergiobelkin.com/images/lastlog-mapa.svg" alt="Mapa de /var/log/lastlog como arreglo indexado por UID. root y los usuarios de sistema comparten el bloque 0; los UID 999, 1000 y 1001 caen en el mismo bloque 71; nobody en el bloque 4671; un usuario de dominio con UID 145.000.000 aterriza en el bloque 10.336.914. El tamaño aparente resultante es de 39,4 GiB contra apenas 16 KiB reales en disco." style="max-width: 100%; height: auto;"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;El archivo visto como un arreglo en disco: cada login escribe su registro en &lt;code&gt;offset = UID × 292&lt;/code&gt;, y los agujeros entre los UID reales no ocupan disco. La frontera administrativa entre cuentas de sistema y de persona (UID 999/1000) cae dentro de un mismo bloque; un solo login de dominio salta a diez millones de bloques de distancia y fija el tamaño aparente.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;El espacio real en disco sigue siendo proporcional a la cantidad de registros escritos. El disco no se llena. Lo que se rompe es todo lo que lea el archivo secuencialmente.&lt;/p&gt;
&lt;p&gt;El manual lo dice sin vueltas:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The lastlog file is a database which contains info on the last login of each user. &lt;strong&gt;You should not rotate it.&lt;/strong&gt; It is a sparse file, so its size on the disk is usually much smaller than the one shown by "ls -l" (which can indicate a really big file if you have in passwd users with a high UID). You can display its real size with "ls -s".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Rotar el archivo con &lt;code&gt;logrotate&lt;/code&gt; usando &lt;code&gt;copy&lt;/code&gt; o &lt;code&gt;copytruncate&lt;/code&gt; implica leerlo y reescribirlo. Los 543,9 GiB aparentes se convierten en 543,9 GiB reales.&lt;/p&gt;
&lt;p&gt;La misma advertencia aplica al backup a nivel de archivo. La receta es excluir &lt;code&gt;/var/log/lastlog&lt;/code&gt; del job, y si hace falta conservar el dato de auditoría, volcarlo a un archivo normal:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;lastlog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;backups&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lastlog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ese archivo pesa lo que corresponde al padrón real de usuarios. El mismo criterio aplica a &lt;code&gt;/var/log/faillog&lt;/code&gt;, que no comparte el formato del registro pero sí el esquema: su código calcula &lt;code&gt;offset = (off_t) uid * sizeof (fl);&lt;/code&gt; y produce un archivo disperso por la misma razón.&lt;/p&gt;
&lt;h3 id="por-que-wtmp-no-tiene-este-problema"&gt;Por qué &lt;code&gt;wtmp&lt;/code&gt; no tiene este problema&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;lastlog&lt;/code&gt; y &lt;code&gt;wtmp&lt;/code&gt; suelen mencionarse juntos, y son modelos opuestos.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lastlog&lt;/code&gt; es una &lt;strong&gt;tabla de estado&lt;/strong&gt;: un slot por usuario, indexado por UID, que se sobrescribe en cada login. Responde "¿cuándo entró por última vez cada usuario?" con acceso directo, y no guarda historia.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wtmp&lt;/code&gt; es un &lt;strong&gt;libro de contabilidad&lt;/strong&gt;: registros &lt;code&gt;struct utmp&lt;/code&gt; que se agregan al final, en orden cronológico. Cada login, logout, arranque o cambio de runlevel suma un registro. Responde "¿qué pasó en este sistema y en qué orden?".&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;lastlog&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;wtmp&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modelo&lt;/td&gt;
&lt;td&gt;arreglo indexado por UID&lt;/td&gt;
&lt;td&gt;log secuencial, append-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escritura&lt;/td&gt;
&lt;td&gt;sobrescribe el slot del UID&lt;/td&gt;
&lt;td&gt;agrega un registro al final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Guarda el nombre?&lt;/td&gt;
&lt;td&gt;no, es la posición&lt;/td&gt;
&lt;td&gt;sí, en &lt;code&gt;ut_user&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Historial&lt;/td&gt;
&lt;td&gt;solo el último login&lt;/td&gt;
&lt;td&gt;todos los eventos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tamaño&lt;/td&gt;
&lt;td&gt;∝ UID máximo (con agujeros)&lt;/td&gt;
&lt;td&gt;∝ cantidad de eventos (denso)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Sparse?&lt;/td&gt;
&lt;td&gt;sí&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;¿Rotar?&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;sí, con &lt;code&gt;logrotate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;struct utmp&lt;/code&gt; mide 384 bytes y sí incluye el nombre (&lt;code&gt;ut_user&lt;/code&gt;) y un campo &lt;code&gt;ut_type&lt;/code&gt; que clasifica el evento. Por eso &lt;code&gt;last&lt;/code&gt; puede reconstruir sesiones completas y mostrar reinicios, algo que &lt;code&gt;lastlog&lt;/code&gt; no puede hacer ni en principio. Crece linealmente con los eventos, así que hay que rotarlo; no tiene agujeros, así que se puede.&lt;/p&gt;
&lt;p&gt;Visto desde arriba, &lt;code&gt;lastlog&lt;/code&gt; es un índice materializado del "último &lt;code&gt;USER_PROCESS&lt;/code&gt; por usuario" que podría derivarse recorriendo &lt;code&gt;wtmp&lt;/code&gt;. La ganancia de ese índice es el acceso O(1). El precio es el archivo disperso.&lt;/p&gt;
&lt;h3 id="donde-esta-esto-hoy"&gt;Dónde está esto hoy&lt;/h3&gt;
&lt;p&gt;En la familia Enterprise Linux el esquema sigue siendo el clásico. RHEL 8, 9 y 10 —y sus derivados Rocky, Alma y Oracle Linux— empaquetan el binario &lt;code&gt;lastlog&lt;/code&gt; en &lt;code&gt;shadow-utils&lt;/code&gt; (4.6, 4.9 y 4.15 respectivamente) y mantienen &lt;code&gt;pam_lastlog&lt;/code&gt; en la pila de PAM. El archivo indexado por UID es lo que hay, y lo que hay que recordar excluir del backup y de la rotación.&lt;/p&gt;
&lt;p&gt;RHEL 10 tiene una particularidad que conviene no dar por sentada. Trae &lt;code&gt;util-linux&lt;/code&gt; 2.40, que es la versión donde upstream incorporó &lt;code&gt;liblastlog2&lt;/code&gt;, pero el paquete se compila con &lt;code&gt;--disable-liblastlog2&lt;/code&gt;: no hay binario &lt;code&gt;lastlog2&lt;/code&gt; ni módulo &lt;code&gt;pam_lastlog2&lt;/code&gt; en los repositorios. Tener la versión no implica tener la funcionalidad.&lt;/p&gt;
&lt;p&gt;Lo que sí cambió en RHEL 10 es el anuncio. Red Hat declaró deprecadas las interfaces &lt;code&gt;utmp&lt;/code&gt; y &lt;code&gt;utmpx&lt;/code&gt; de glibc y avisó que se eliminan en RHEL 11. El motivo que da es el desbordamiento de su contador en 2106; el de &lt;code&gt;lastlog&lt;/code&gt;, con su &lt;code&gt;ll_time&lt;/code&gt; de 32 bits con signo, vence bastante antes.&lt;/p&gt;
&lt;p&gt;Fedora ya hizo el cambio. Desde Fedora 43 el default del sistema es &lt;code&gt;lastlog2&lt;/code&gt;, con &lt;code&gt;pam_lastlog2&lt;/code&gt; en la pila de PAM. En una Fedora 44 al día, el paquete &lt;code&gt;shadow-utils&lt;/code&gt; ya no provee un binario &lt;code&gt;lastlog&lt;/code&gt;; el que hay es &lt;code&gt;lastlog2&lt;/code&gt;, y viene de &lt;code&gt;util-linux&lt;/code&gt;. Los datos están en &lt;code&gt;/var/lib/lastlog/lastlog2.db&lt;/code&gt;, una base SQLite indexada por nombre de usuario, y &lt;code&gt;/var/log/lastlog&lt;/code&gt; queda como un archivo vacío de 0 bytes.&lt;/p&gt;
&lt;p&gt;El esquema de esa base dice, en una línea, todo lo que cambió:&lt;/p&gt;
&lt;p&gt;&lt;a class="image-reference" href="https://sergiobelkin.com/images/lastlog2-schema.png"&gt;&lt;img src="https://sergiobelkin.com/images/lastlog2-schema.png" alt="Sesión de sqlite3 en Fedora 44 sobre la base lastlog2.db. El comando .schema devuelve: CREATE TABLE Lastlog2(Name TEXT PRIMARY KEY, Time INTEGER, TTY TEXT, RemoteHost TEXT, Service TEXT);" style="max-width: 100%; height: auto;"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Name TEXT PRIMARY KEY&lt;/code&gt;: el nombre del usuario está guardado y es la clave. La identidad dejó de ser la posición, que es justamente lo que hacía sparse al archivo viejo y lo que provocaba las colisiones de UID. Sin índice posicional no hay agujeros que llenar ni tamaño aparente que explicar. &lt;code&gt;Time INTEGER&lt;/code&gt; es un entero de 64 bits, sin la condicional por arquitectura ni el tope de 2038. Y aparece un campo que la &lt;code&gt;struct&lt;/code&gt; no tenía: &lt;code&gt;Service&lt;/code&gt;, el servicio PAM que originó la sesión.&lt;/p&gt;
&lt;p&gt;En cualquiera de los dos casos, qué esquema tenés a mano se comprueba con un comando:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;rpm -ql shadow-utils | grep -i lastlog
&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="fuentes-y-mas-recursos"&gt;Fuentes y más recursos&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/lastlog.8.html"&gt;&lt;code&gt;lastlog(8)&lt;/code&gt;&lt;/a&gt; — la fuente de la advertencia "You should not rotate it" y de la nota sobre el archivo sparse y &lt;code&gt;ls -s&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man5/utmp.5.html"&gt;&lt;code&gt;utmp(5)&lt;/code&gt;&lt;/a&gt; — define &lt;code&gt;struct utmp&lt;/code&gt;, sus campos &lt;code&gt;ut_type&lt;/code&gt; y &lt;code&gt;ut_user&lt;/code&gt;, y la diferencia entre &lt;code&gt;/run/utmp&lt;/code&gt; (sesiones activas) y &lt;code&gt;/var/log/wtmp&lt;/code&gt; (historial).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 5 sssd-ldap&lt;/code&gt; — sección &lt;em&gt;ID MAPPING&lt;/em&gt;: los defaults &lt;code&gt;ldap_idmap_range_min&lt;/code&gt;, &lt;code&gt;ldap_idmap_range_max&lt;/code&gt; y &lt;code&gt;ldap_idmap_range_size&lt;/code&gt; que ubican los UID de dominio en el rango alto.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fedoraproject.org/wiki/Changes/Migrate_to_lastlog2"&gt;Fedora Change: Migrate to lastlog2&lt;/a&gt; — la migración a &lt;code&gt;lastlog2&lt;/code&gt; como default del sistema, desde Fedora 43.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/10.0_release_notes/deprecated-features"&gt;RHEL 10 — Deprecated functionality&lt;/a&gt; — la deprecación de las interfaces &lt;code&gt;utmp&lt;/code&gt; y &lt;code&gt;utmpx&lt;/code&gt; de glibc, con la eliminación anunciada para RHEL 11.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/redhat/centos-stream/rpms/util-linux/-/raw/c10s/util-linux.spec"&gt;&lt;code&gt;util-linux.spec&lt;/code&gt; de CentOS Stream 10&lt;/a&gt; — el &lt;code&gt;--disable-liblastlog2&lt;/code&gt; que deja a RHEL 10 sin &lt;code&gt;lastlog2&lt;/code&gt; pese a tener util-linux 2.40.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man8/pam_lastlog2.8.html"&gt;&lt;code&gt;pam_lastlog2(8)&lt;/code&gt;&lt;/a&gt; — el módulo PAM que reemplaza a &lt;code&gt;pam_lastlog&lt;/code&gt;: Y2038-safe y con backend SQLite3, en util-linux desde 2.40.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="la-serie"&gt;La serie&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parte&lt;/th&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;De qué trata&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://sergiobelkin.com/posts/sparse-files-linux/"&gt;Archivos sparse en Linux&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Los agujeros: cómo un archivo declara un tamaño y ocupa otro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Particularidades de lastlog&lt;/strong&gt; &lt;em&gt;(estás acá)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;El UID como posición dentro del archivo, y todo lo que se desprende de esa decisión&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://sergiobelkin.com/posts/lastlog-y2038/"&gt;Una máquina de 64 bits no es inmune al 2038&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;El campo de fecha de 32 bits, lo que hizo glibc y el reemplazo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description><category>filesystems</category><category>lastlog</category><category>sparse-files</category><category>SSSD</category><category>sysadmin</category><guid>https://sergiobelkin.com/posts/lastlog-particularidades/</guid><pubDate>Fri, 17 Jul 2026 10:00:00 GMT</pubDate></item></channel></rss>