3 Power Tips + Power Link I13

Leer un comando antes de correrlo y autorizarlo por su nombre son decisiones que se toman sobre un texto. Lo que el proceso puede hacer una vez que arrancó se decide en otro lado.

Power Tip #1 El comando que leés no es el que corre

El script del pipeline está versionado y lo revisaste entero. Dice $CLEAN "$d" y en el repositorio no aparece dónde se define CLEAN.

  • No aparece porque no vive ahí: lo inyecta la plataforma de CI, desde la configuración del job o desde un grupo de variables que se edita en otra pantalla.
  • Una función viaja igual de callada. export -f ls la manda al ambiente, y en el hijo esa función le gana al binario del mismo nombre.
  • El alias, en cambio, no viaja: en un script no se expande salvo que lo habilites. Es el único de los tres que se queda donde lo ves.
env | grep -E '^CLEAN=|BASH_FUNC'   # como paso del job: lo que el script usa y no declara

Leer el repositorio asume que el repositorio es el programa. La otra mitad la pone el ambiente donde corre el job y no está versionada, así que queda fuera del alcance de cualquier revisión del código. El nombre y lo que ejecuta son dos cosas distintas hasta que los resolvés en el proceso donde va a correr.

Power Tip #2 Permitir un comando es permitir todo lo que ese programa es capaz de hacer

La regla de sudoers autoriza less /var/log/secure y nada más. Leer un log con un visor: se abre, se lee, se cierra. Parece prudente y acotado.

  • less no solo muestra. Con ! corre un comando en una shell —la que diga $SHELL—, con | manda el contenido a otro programa y con v abre el editor. No es un agujero: está en su manual, en la lista de comandos.
  • Tampoco es una particularidad de less. vim sale a la shell con :!, find ejecuta programas con -exec, tar canaliza lo que extrae a un programa externo con --to-command. Ninguno está roto: todos hacen lo que su documentación dice.
  • "Solo lectura" describe para qué lo autorizaste, no lo que el programa puede hacer una vez abierto. La regla nombra un archivo y entrega todo lo que ese archivo es capaz de hacer, incluido lo que le delegue a otro.
sudo -l   # de cada línea: ¿qué más puede hacer ese programa, y a quién llama?

Enumerar lo que se puede correr te ahorra tener que prever cada cosa que no querés que corra, y eso no es poco. Lo que no te da es un límite a lo que esos comandos pueden hacer: el nombre autorizado no dice hasta dónde llega el proceso una vez que arrancó, y averiguarlo binario por binario no escala. Ese límite hay que ponerlo en otro lado.

Power Tip #3 sudo dice quién; systemd dice hasta dónde

El job de mantenimiento corre autorizado por una regla de sudoers que nombra un solo script. Mientras corre, tiene el filesystem entero a mano, /home incluido.

  • La regla contestó una pregunta —quién puede correr qué— y no hay nada en ella que conteste la otra. El alcance del proceso no se decide en sudoers porque sudoers no habla de eso.
  • La otra pregunta se responde en la unidad. ProtectHome=, ProtectSystem=, PrivateTmp= le recortan al proceso lo que puede tocar, sin tocar la autorización ni el script. No compiten con sudo: arrancan donde sudo termina.
  • Ahora, no le creas al archivo. En scope de usuario, según la versión de systemd, esas propiedades pueden necesitar además PrivateUsers=yes para hacer efecto — sin error, y con systemctl show confirmándote que están puestas.
systemd-run --user --pty --property=ProtectHome=yes ls -a $HOME   # si te lista el home, acá no rige

Las dos preguntas se responden en lugares distintos y ninguna cubre la de la otra. Antes de apoyarte en la segunda, hacela fallar a propósito: pedile al proceso lo que no tendría que poder, y fijate si puede.

La discusión sobre IA y software libre suele quedarse en si el código generado sirve o no. Debian se está haciendo una pregunta más de fondo: quién puede contribuir y con qué. La resolución general en debate hasta el 8 de agosto junta seis propuestas en la misma votación, desde prohibir las contribuciones asistidas por LLM en el Contrato Social hasta no pronunciarse y dejarlo al criterio de cada quien: General Resolution: LLM usage in Debian. Cuando esto se publica todavía no se votó, que es el mejor momento para leerlas: las seis están argumentadas y firmadas, cosa poco frecuente en una discusión sobre este tema.

Una máquina de 64 bits no es inmune al 2038

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í.

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.

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 Dónde está parada tu máquina 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.

Ese número se guarda en un tipo de dato llamado time_t, y de cuántos bits tenga depende hasta qué fecha puede seguir contando. Un time_t 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.

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 time_t es uno de ellos.

Por eso en un servidor x86-64 —cualquiera con procesador Intel o AMD de las últimas dos décadas— time_t 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.

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 time_t: 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 /var/log/lastlog, donde queda registrado el último inicio de sesión de cada usuario, es uno de esos formatos.

Si compilás esto en cualquier Fedora o RHEL:

#include <lastlog.h>
#include <stdio.h>

int main(void) {
    struct lastlog ll;
    ll.ll_time = -1;
    printf("registro: %zu bytes | ll_time: %zu bytes, %s\n",
           sizeof ll, sizeof ll.ll_time,
           (ll.ll_time < 0) ? "con signo" : "sin signo");
    return 0;
}

No abre /var/log/lastlog ni necesita privilegios: lo único que hace es preguntarle al compilador qué forma tiene la estructura en esa máquina. sizeof 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 -1 al campo y se pregunta si quedó negativo. Si el campo tiene signo, -1 sigue valiendo -1; si no lo tiene, ese mismo patrón de bits se lee como 4.294.967.295 y la comparación da falso.

En una Fedora 44 con glibc 2.43 imprime:

registro: 292 bytes | ll_time: 4 bytes, sin signo

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.

Por qué el campo quedó en 32 bits

struct lastlog la define glibc, no shadow —el paquete de las herramientas de cuentas de usuario, useradd, chage, passwd, y del que en RHEL sale también el comando lastlog—. 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.

Su declaración actual tiene una condición:

#if __WORDSIZE_TIME64_COMPAT32
    __uint32_t ll_time;
#else
    __time_t ll_time;
#endif

__WORDSIZE_TIME64_COMPAT32 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 struct lastlog tuviera un ll_time 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 /var/log/lastlog: 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 time_t de la plataforma sea de 64.

Es una decisión sobre el formato en disco, 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 las particularidades de lastlog.

El problema, tal como era

Hasta hace poco ese campo de 4 bytes era con signo, y de ahí sale la fecha famosa.

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:

2038-01-19T03:14:07+00:00

Un segundo después desborda al valor negativo más chico. Y la ruta de lectura de shadow hace exactamente esto:

time_t ll_time;
...
ll_time = ll.ll_time;

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 extensión de signo, 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

1901-12-13T20:45:52+00:00

La escritura tiene la falla simétrica, y llega por dos caminos. En el propio comando, cuando lastlog -S fija una entrada a mano, es una sola línea: ll.ll_time = NOW;, donde NOW es time((time_t *) 0). En cada inicio de sesión el que escribe es el módulo pam_lastlog, que hace lo mismo en dos pasos: time(&ll_time) sobre un time_t de 64 bits y last_login.ll_time = ll_time; sobre el campo de 32. En ambos casos el valor se trunca al guardarse.

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.

Lo que hizo glibc

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:

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 <utmp.h> and <utmpx.h> (except for login_tty) due to locking and session management problems.

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:

2106-02-07T06:28:15+00:00

Y la extensión ahora es de ceros, no de signo: ll_time = ll.ll_time; sobre un __uint32_t produce un time_t positivo. El login de 2038 se lee como 2038.

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.

Es la razón por la que el programa del principio imprime sin signo. Donde el campo siga declarado como int32_t imprimirá con signo, 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.

Lo que ese cambio gana, y lo que no

El cambio se lee de dos maneras equivocadas: "el problema está resuelto" o "fue un parche inútil". No es ninguna de las dos.

Gana 68 años sin tocar el formato. El registro sigue midiendo 292 bytes, los offsets siguen siendo UID × 292, 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.

No ensancha nada. El campo sigue teniendo 32 bits. La pared se corrió, no desapareció.

No es una maniobra exclusiva de glibc. MariaDB llegó a la misma pared y salió por la misma puerta: desde su versión 11.5, el tipo TIMESTAMP pasó de 2038 a 2106 reinterpretando como sin signo el mismo campo de cuatro bytes, sin tocar el formato en disco —"storage of timestamp is not affected by the change", 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.

Cambia el significado de un valor sin que el archivo lo declare. 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.

Por qué no bastaba con agrandar el campo

Si el campo pasara de 4 a 8 bytes, sizeof(struct lastlog) pasaría de 292 a 296. Pero el archivo no guarda el nombre del usuario: la identidad de cada registro es su posición, calculada como UID × sizeof(struct lastlog). 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.

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 unsigned fue la única maniobra posible sin romper compatibilidad, y funciona porque no cambia ni un byte del formato.

El arreglo de verdad: lastlog2

lastlog2 es un proyecto de Thorsten Kukuk integrado en util-linux 2.40. Abandona el archivo indexado por posición y guarda los datos en una base SQLite3 en /var/lib/lastlog/lastlog2.db, con una tabla cuya clave es el nombre de usuario:

CREATE TABLE Lastlog2(
    Name       TEXT PRIMARY KEY,
    Time       INTEGER,
    TTY        TEXT,
    RemoteHost TEXT,
    Service    TEXT
);

Escribir un login es un REPLACE INTO por nombre; leer es un SELECT 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, pam_lastlog2, que reemplaza a pam_lastlog.

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 archivo disperso 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 Service no existía en struct lastlog y no podría haberse agregado.

Una aclaración cronológica: la justificación que da el propio proyecto —"the standard /var/log/lastlog implementation using lastlog.h from glibc uses a 32bit time_t"— se escribió cuando el campo todavía era con signo. El cambio de glibc corrió la fecha, no eliminó el motivo.

Fedora migró a lastlog2 como default del sistema en su versión 43. En una Fedora 44 el binario lastlog ya no existe, /var/log/lastlog queda como un archivo vacío, y rpm -ql shadow-utils | grep -i lastlog no devuelve nada. glibc, en cambio, sigue instalando <lastlog.h>: la estructura sobrevive al programa.

utmp y wtmp corren la misma suerte

El cambio de glibc 2.40 alcanzó también a struct utmp y struct utmpx, que sostienen /run/utmp, /var/log/wtmp y /var/log/btmp. Se verifica con el mismo programa, cambiando <lastlog.h> por <utmp.h>, la estructura por struct utmp y el campo por ut_tv.tv_sec:

sizeof(struct utmp) = 384 | ut_tv.tv_sec = 4 bytes, sin signo

La diferencia es que ahí no hay un lastlog2 esperando. La dirección de upstream, sostenida por el mismo autor, es dejar de escribir utmp y consultar a systemd-logind a través de las funciones sd_*() de libsystemd. Es un reemplazo más ambicioso que cambiar un backend de almacenamiento, y por eso avanza más despacio.

Dónde está parada tu máquina

Tres comprobaciones, ninguna destructiva.

La primera 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 glibc-devel:

grep -A6 'struct lastlog' /usr/include/bits/utmp.h

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: int32_t significa 2038, __uint32_t significa 2106. La segunda rama, __time_t, es la de aarch64, donde el campo mide 8 bytes y el problema no existe.

La segunda:

ls -l /var/lib/lastlog/lastlog2.db

Si existe, el sistema ya está en el esquema SQLite y el problema no aplica.

La tercera:

ls -ls /var/log/lastlog

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.

Lo que conviene no hacer es deducirlo del número de versión. rpm -q glibc 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:

Sistema glibc Campo Límite
RHEL 8.10 2.28 int32_t 2038
RHEL 9.8 2.34 int32_t 2038
RHEL 10.2 2.39 __uint32_t 2106

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 2.39-12 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 ubi8, ubi9 y ubi10, que llevan la glibc de cada RHEL.

Lo mismo en una máquina de 32 bits

Nada de esto es propio de RHEL. En una Debian 12 sobre i386 el mismo grep devuelve int32_t. 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 lastlog. 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.

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).

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.

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, dejó a i386 deliberadamente afuera. 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 lastlog 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 lastlog no, porque recompilar no cambia lo que ya está escrito en disco.

ls -l /var/lib/lastlog/lastlog2.db

Si existe, el sistema ya está en el esquema SQLite y el problema no aplica.

ls -ls /var/log/lastlog

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.

Para qué se corrió la pared

Los 68 años que agregó el cambio no son para llegar a 2106. Nadie va a estar leyendo /var/log/lastlog 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 (epoch), 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.

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.

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.

Fuentes y más recursos

La serie

Parte Post De qué trata
1 Archivos sparse en Linux Los agujeros: cómo un archivo declara un tamaño y ocupa otro
2 Particularidades de lastlog El UID como posición dentro del archivo, y todo lo que se desprende de esa decisión
3 Una máquina de 64 bits no es inmune al 2038 (estás acá) El campo de fecha de 32 bits, lo que hizo glibc y el reemplazo

Particularidades de lastlog

En un RHEL 8 recién instalado, /var/log/lastlog 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, ls -l puede informar un archivo de cientos de gigabytes. du -h sobre ese mismo archivo devuelve unos pocos kilobytes.

El archivo es sparse, y eso explica la diferencia entre los dos números. Lo que explica por qué es sparse es una sola línea de código.

El UID es la dirección

/var/log/lastlog 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 es el UID. En shadow-utils el cálculo aparece idéntico en la ruta de lectura y en la de escritura:

offset = (off_t) pw->pw_uid * sizeof (ll);

El archivo se trata como un arreglo en disco cuyo índice es el UID, exactamente igual que array[i] vive en base + i * sizeof(elemento). Para leer el último login de cualquier usuario se hace un fseeko a ese offset y se lee un registro. Acceso directo, sin parsear nada, sin mantener una estructura auxiliar.

El (off_t) merece atención. La conversión está antes 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.

El registro, y por qué mide lo que mide

La struct lastlog no la define shadow: viene de glibc —se declara en <bits/utmp.h>, que un programa obtiene incluyendo <lastlog.h>—. Son tres campos:

Offset Campo Tamaño Contenido
0 ll_time 4 bytes segundos desde epoch del último login
4 ll_line 32 bytes terminal: pts/0, tty1
36 ll_host 256 bytes host remoto
total 292 bytes

Ese 292 no es universal, y ahí hay una particularidad que se pasa por alto. El campo ll_time está definido bajo una condición:

#if __WORDSIZE_TIME64_COMPAT32
    __uint32_t ll_time;
#else
    __time_t ll_time;
#endif

En x86-64 esa macro vale 1, ll_time ocupa 4 bytes y el registro mide 292. En aarch64 vale 0, ll_time pasa a ser un __time_t de 8 bytes, y el registro mide 296. Mismo archivo, mismo programa, distinta grilla.

La consecuencia práctica es que /var/log/lastlog 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 ll_time condicional además arrastra el problema del año 2038.

El nombre del usuario no está guardado

Mirá de nuevo la tabla: hay tiempo, terminal y host. No hay nombre. La identidad del usuario no está en el registro, está en la posición del registro. Si el offset es UID × 292, entonces UID = offset / 292, y eso es todo lo que el archivo sabe sobre quién es quién.

De ahí sale un comportamiento que sorprende cuando aparece. Si dos entradas de /etc/passwd comparten UID:

alice:x:1000:1000::/home/alice:/bin/bash
bob:x:1000:1000::/home/bob:/bin/bash

las dos cuentas comparten físicamente el mismo registro. lastlog recorre /etc/passwd con getpwent() y procesa cada entrada por separado, así que imprime dos filas, con nombres distintos y exactamente la misma fecha, la misma terminal y el mismo host. Si entró alice, la salida muestra a bob con esa fecha.

En escritura es peor de razonar. lastlog -S -u bob hace getpwnam("bob") 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 alice.

No es un defecto de lastlog. 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 /etc/passwd. Dos nombres con el mismo UID son el mismo usuario a todos los efectos, y lastlog hereda esa ambigüedad en lugar de inventarse una propia.

Un corolario del mismo diseño: como lastlog recorre passwd en vez de recorrer el archivo, los registros de usuarios ya borrados siguen físicamente en disco pero no se listan. Y si nsswitch está respaldado por SSSD contra un directorio de decenas de miles de usuarios, ese getpwent() enumera toda la base remota. El cuello de botella pasa a ser NSS, no el archivo.

Leer es seguro; escribir es lo que infla

Hay una asimetría en el código que determina cuándo el archivo crece.

En lectura, antes de posicionarse se comprueba que el registro entre dentro del archivo:

if (offset + sizeof (ll) <= statbuf.st_size)

Si el UID cae más allá del final, no se lee nada: la estructura se llena de ceros con memzero y se imprime **Never logged in**. Consultar el lastlog de un usuario con UID enorme no 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.

En escritura no hay ninguna comprobación de límites: se hace fseeko al offset y se escribe. Un solo login —o un solo lastlog -S -u <usuario>— con un UID alto extiende el archivo hasta ese offset y crea el agujero intermedio.

Por eso el tamaño aparente del archivo no depende de cuántos usuarios tenés, sino del UID más alto que alguna vez haya escrito un registro.

Los números con un directorio corporativo

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 ldap_idmap_range_min = 200000 y ldap_idmap_range_max = 2000200000, repartido en slices de 200000, 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.

A 292 bytes por registro:

UID que inició sesión Tamaño aparente
1 000 285 KiB
1 000 000 278 MiB
100 000 000 27,2 GiB
145 000 000 39,4 GiB
2 000 199 999 (tope del rango) 543,9 GiB

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.

El archivo visto como un arreglo en disco: cada login escribe su registro en offset = UID × 292, 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.

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.

El manual lo dice sin vueltas:

The lastlog file is a database which contains info on the last login of each user. You should not rotate it. 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".

Rotar el archivo con logrotate usando copy o copytruncate implica leerlo y reescribirlo. Los 543,9 GiB aparentes se convierten en 543,9 GiB reales.

La misma advertencia aplica al backup a nivel de archivo. La receta es excluir /var/log/lastlog del job, y si hace falta conservar el dato de auditoría, volcarlo a un archivo normal:

lastlog > /var/backups/lastlog.txt

Ese archivo pesa lo que corresponde al padrón real de usuarios. El mismo criterio aplica a /var/log/faillog, que no comparte el formato del registro pero sí el esquema: su código calcula offset = (off_t) uid * sizeof (fl); y produce un archivo disperso por la misma razón.

Por qué wtmp no tiene este problema

lastlog y wtmp suelen mencionarse juntos, y son modelos opuestos.

lastlog es una tabla de estado: 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.

wtmp es un libro de contabilidad: registros struct utmp 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?".

lastlog wtmp
Modelo arreglo indexado por UID log secuencial, append-only
Escritura sobrescribe el slot del UID agrega un registro al final
¿Guarda el nombre? no, es la posición sí, en ut_user
Historial solo el último login todos los eventos
Tamaño ∝ UID máximo (con agujeros) ∝ cantidad de eventos (denso)
¿Sparse? no
¿Rotar? no sí, con logrotate

struct utmp mide 384 bytes y sí incluye el nombre (ut_user) y un campo ut_type que clasifica el evento. Por eso last puede reconstruir sesiones completas y mostrar reinicios, algo que lastlog no puede hacer ni en principio. Crece linealmente con los eventos, así que hay que rotarlo; no tiene agujeros, así que se puede.

Visto desde arriba, lastlog es un índice materializado del "último USER_PROCESS por usuario" que podría derivarse recorriendo wtmp. La ganancia de ese índice es el acceso O(1). El precio es el archivo disperso.

Dónde está esto hoy

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 lastlog en shadow-utils (4.6, 4.9 y 4.15 respectivamente) y mantienen pam_lastlog 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.

RHEL 10 tiene una particularidad que conviene no dar por sentada. Trae util-linux 2.40, que es la versión donde upstream incorporó liblastlog2, pero el paquete se compila con --disable-liblastlog2: no hay binario lastlog2 ni módulo pam_lastlog2 en los repositorios. Tener la versión no implica tener la funcionalidad.

Lo que sí cambió en RHEL 10 es el anuncio. Red Hat declaró deprecadas las interfaces utmp y utmpx de glibc y avisó que se eliminan en RHEL 11. El motivo que da es el desbordamiento de su contador en 2106; el de lastlog, con su ll_time de 32 bits con signo, vence bastante antes.

Fedora ya hizo el cambio. Desde Fedora 43 el default del sistema es lastlog2, con pam_lastlog2 en la pila de PAM. En una Fedora 44 al día, el paquete shadow-utils ya no provee un binario lastlog; el que hay es lastlog2, y viene de util-linux. Los datos están en /var/lib/lastlog/lastlog2.db, una base SQLite indexada por nombre de usuario, y /var/log/lastlog queda como un archivo vacío de 0 bytes.

El esquema de esa base dice, en una línea, todo lo que cambió:

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);

Name TEXT PRIMARY KEY: 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. Time INTEGER es un entero de 64 bits, sin la condicional por arquitectura ni el tope de 2038. Y aparece un campo que la struct no tenía: Service, el servicio PAM que originó la sesión.

En cualquiera de los dos casos, qué esquema tenés a mano se comprueba con un comando:

rpm -ql shadow-utils | grep -i lastlog

Fuentes y más recursos

  • lastlog(8) — la fuente de la advertencia "You should not rotate it" y de la nota sobre el archivo sparse y ls -s.
  • utmp(5) — define struct utmp, sus campos ut_type y ut_user, y la diferencia entre /run/utmp (sesiones activas) y /var/log/wtmp (historial).
  • man 5 sssd-ldap — sección ID MAPPING: los defaults ldap_idmap_range_min, ldap_idmap_range_max y ldap_idmap_range_size que ubican los UID de dominio en el rango alto.
  • Fedora Change: Migrate to lastlog2 — la migración a lastlog2 como default del sistema, desde Fedora 43.
  • RHEL 10 — Deprecated functionality — la deprecación de las interfaces utmp y utmpx de glibc, con la eliminación anunciada para RHEL 11.
  • util-linux.spec de CentOS Stream 10 — el --disable-liblastlog2 que deja a RHEL 10 sin lastlog2 pese a tener util-linux 2.40.
  • pam_lastlog2(8) — el módulo PAM que reemplaza a pam_lastlog: Y2038-safe y con backend SQLite3, en util-linux desde 2.40.

La serie

Parte Post De qué trata
1 Archivos sparse en Linux Los agujeros: cómo un archivo declara un tamaño y ocupa otro
2 Particularidades de lastlog (estás acá) El UID como posición dentro del archivo, y todo lo que se desprende de esa decisión
3 Una máquina de 64 bits no es inmune al 2038 El campo de fecha de 32 bits, lo que hizo glibc y el reemplazo

Archivos sparse en Linux

Sparse files en la práctica

Un sparse file se entiende creándolo y mirándolo, no leyendo sobre él. Vamos a tratar un archivo como el disco de una máquina virtual: se crea a tamaño completo y el guest le escribe encima. En la terminal, esas escrituras las simulamos con dd.

El "disco" de 1 GiB, todavía en blanco:

truncate -s 1G disk.img

Lo medimos de dos maneras —lo que declara y lo que ocupa de verdad—:

du -h --apparent-size disk.img    # 1,0G  ← lo que declara
du -h disk.img                    # 0     ← lo que ocupa

Declara un gigabyte y ocupa cero. Escribimos 16 MiB al principio, como el guest grabando su arranque:

dd if=/dev/urandom of=disk.img bs=1M count=16 seek=0 conv=notrunc status=none
du -h --apparent-size disk.img    # 1,0G
du -h disk.img                    # 16M

El tamaño declarado no se movió; el real subió los 16 MiB exactos que escribimos. Ahora otros 16 MiB, pero lejos, en el offset 512 MiB:

dd if=/dev/urandom of=disk.img bs=1M count=16 seek=512 conv=notrunc status=none
du -h disk.img                    # 32M

Real 32 MiB, aparente todavía 1 GiB. Esos 32 MiB reales son solo las dos zonas de 16 que escribimos; entre ellas quedó un agujero de casi 500 MiB —espacio que el archivo declara pero para el que nunca se pidió un bloque en disco—.

Ahora al derecho y al revés. Llenamos parte del agujero, 128 MiB en el offset 200 MiB:

dd if=/dev/urandom of=disk.img bs=1M count=128 seek=200 conv=notrunc status=none
du -h disk.img                    # 160M

El real saltó a 160 MiB —los 32 de antes más los 128 nuevos—: escribir en un agujero cuesta disco. Y lo perforamos de vuelta, con fallocate:

fallocate --punch-hole --offset 200M --length 128M disk.img
du -h disk.img                    # 32M

El real volvió a 32 MiB: perforar libera los bloques y los devuelve al disco. Ese tramo, ahora vacío, se lee como ceros —no como basura, ni con error—:

dd if=disk.img bs=1M skip=200 count=128 status=none | tr -d '\0' | wc -c    # 0

En todo el recorrido el tamaño aparente no se movió de 1 GiB; el real subió al escribir y bajó al perforar. Son las dos medidas de un sparse file: lo que declara y lo que realmente ocupa en disco.

Las dos direcciones del recorrido, que es lo que conviene no confundir:

Operación Partís de Terminás en st_blocks
escribir (dd … seek=) agujero datos en disco sube
perforar (--punch-hole) datos en disco agujero (se lee como ceros) baja

Perforar no escribe ceros: libera los bloques. Que el agujero se lea después como ceros es el efecto observable; el mecanismo es la desasignación, y por eso el espacio real baja en vez de subir.

Dos tamaños, no uno

El lab mostró dos números que se mueven por separado. Tienen nombre, y el inodo guarda los dos; stat los muestra juntos:

stat -c 'aparente=%s bytes | reales=%b bloques de %B bytes' disk.img
  • st_size (el "aparente") es el offset del último byte del archivo más uno. Es lo que responde ls -l. Es una declaración sobre el espacio de direcciones lógico del archivo.
  • st_blocks (el "real") es el espacio que el archivo ocupa de verdad, contado —en Linux— en unidades fijas de 512 bytes. Es lo que informa du.

El archivo sparse del lab como una tira de 1 GiB: dos bloques de datos de 16 MiB en terracota, en los offsets 0 y 512 MiB, y el resto agujeros punteados. El tamaño aparente (st_size) es 1 GiB; el real (st_blocks), 32 MiB.

El mismo archivo, sus dos medidas: st_size declara 1 GiB —el ancho lógico completo—; st_blocks cuenta solo los dos bloques tocados. Entre los datos, agujeros: espacio declarado que no ocupa disco.

Un archivo denso cualquiera muestra las dos magnitudes juntas. /usr/bin/chmod mide 65920 bytes (st_size), pero en disco ocupa 69632: el filesystem asigna bloques enteros de 4096, y 65920 bytes necesitan 17 de esos bloques (17 × 4096 = 69632), con 3712 bytes del último reservados pero sin usar.1 En un archivo denso lo asignado iguala o supera al tamaño.

Un sparse file es el caso opuesto: st_size mayor que el espacio asignado, y la diferencia son los agujeros que vimos abrirse y cerrarse en el lab.

De dónde salen en la práctica

Los agujeros no son una curiosidad de laboratorio. Aparecen solos, sin que nadie los pida, cada vez que un programa escribe lejos del principio del archivo o libera rangos intermedios:

  • Imágenes de disco de máquinas virtuales. Un disco raw de 100 GiB con 8 GiB usados es un sparse file. qcow2 hace lo suyo por encima, pero el archivo subyacente también lo es.
  • Bases de datos que preasignan un archivo grande y lo llenan de a poco.
  • Clientes de BitTorrent, que crean el archivo con su tamaño final y van completando piezas fuera de orden.
  • Core dumps, que no vuelcan las regiones de memoria sin mapear.
  • /var/log/lastlog, que usa el UID del usuario como posición dentro del archivo — el caso más didáctico de todos.

Los filesystems que un sysadmin corporativo tiene enfrente —ext4, XFS, btrfs— soportan agujeros. También tmpfs. FAT y exFAT no, y son justo los que aparecen en un pendrive o en la partición EFI: copiar un sparse file a uno de ellos lo escribe entero, porque el destino no tiene forma de representar los agujeros.

Ver dónde están los agujeros

Para ver dónde están los agujeros —qué rangos tienen datos y cuáles son hueco—, xfs_io (del paquete xfsprogs) los marca con hole. Se apoya en FIEMAP, un ioctl genérico del kernel que ext4, XFS y btrfs implementan por igual, así que, a pesar de su nombre, sirve en los tres.

xfs_io -c fiemap disk.img
  0: [0..32767]: …
  1: [32768..1048575]: hole
  2: [1048576..1081343]: …
  3: [1081344..2097151]: hole

La tercera columna —los offsets físicos en disco— la recorté con : cambia según el filesystem y no aporta acá. Los rangos van en sectores de 512 bytes: los primeros 16 MiB ([0..32767]) tienen datos, el tramo [32768..1048575] es un hole, en el offset 512 MiB ([1048576..1081343]) están los otros 16 MiB, y el resto hasta el final es otro hole.

Hay un camino más de bajo nivel: los flags SEEK_HOLE y SEEK_DATA de lseek (desde Linux 3.1) saltan al próximo agujero o al próximo rango con datos sin leer el archivo entero. Es el mecanismo que usan por dentro las herramientas que preservan agujeros al copiar, y el que las separa de las que re-inflan.

Una advertencia sobre filefrag, que suele ser lo primero que uno prueba: su conteo (N extents found) mide fragmentación física, no agujeros, y se lee distinto según el filesystem —el mismo archivo con el mismo agujero da "1 extent" en XFS y "2" en btrfs—. Para ver agujeros, fiemap.

Convertir ceros en agujeros

En el lab abrimos agujeros con --punch-hole. La otra variante, --dig-holes, recupera espacio: toma un archivo que ya ocupa disco lleno de ceros —una copia que se infló, una imagen descargada sin sparse— y reemplaza esas secuencias de ceros por agujeros. Un archivo de 64 MiB escritos en cero ocupa 64 MiB reales; después, cero:

dd if=/dev/zero of=lleno.img bs=1M count=64 status=none
du -h lleno.img                   # 64M
fallocate --dig-holes lleno.img
du -h lleno.img                   # 0   (el aparente sigue en 64M)

Con --dig-holes se cierra el cuadro de las tres operaciones. --punch-hole y --dig-holes terminan igual —un agujero, st_blocks baja—, pero parten de cosas distintas:

Operación Partís de Terminás en st_blocks
escribir ceros (dd if=/dev/zero) agujero ceros reales en disco sube
perforar (--punch-hole) datos en disco agujero baja
cavar (--dig-holes) ceros reales en disco agujero baja

Escribir ceros y cavarlos son inversas: la primera los materializa en disco, la segunda los recupera. Perforar es el atajo cuando ni siquiera te importa qué había en el rango.

Cuándo se convierte en un problema

El diseño se sostiene mientras el archivo se quede quieto. El problema aparece cuando algo lo copia, porque una herramienta que lee a través de la API normal de bytes ve ceros, no agujeros. Si al escribir no vuelve a perforarlos, los materializa.

Con el disk.img de arriba —1 GiB aparente, 32 MiB reales—, copiado en RHEL 8 (XFS, coreutils 8.30):

Operación Ocupación en destino
cp disk.img destino 64 MiB
cp --sparse=never disk.img destino 1 GiB
cat disk.img > destino 1 GiB
tar cf plain.tar disk.img y extraer 1 GiB (y el .tar pesa 1,1 GiB)
tar cSf sparse.tar disk.img y extraer 64 MiB (y el .tar pesa 33 MiB)
rsync -a disk.img destino 1 GiB
rsync -aS disk.img destino 64 MiB

Las cuatro que re-inflan —cp --sparse=never, cat, tar sin -S, rsync -a— llevan la copia a 1 GiB: materializan cada cero.2 Las tres que preservan el sparse —cp a secas, tar -S, rsync -aS— dan 64 MiB. Que cp sin flags preserve ya es contraintuitivo: su default es --sparse=auto, detecta los agujeros del origen y los recrea; solo los pierde con --sparse=never o si el destino no los soporta.

Pero 64 MiB es el doble de los 32 del origen, y no es re-inflado: los agujeros están intactos (xfs_io -c fiemap destino los muestra igual), y ese doble es XFS reservando bloques de más al escribir (speculative preallocation). Es una reserva transitoria —XFS la reclama con el tiempo (el barredor de eofblocks) o bajo presión de espacio, y su tamaño depende de la opción de montaje allocsize y de la versión—, así que el número exacto puede no reproducirse igual en otro XFS. En ext4 o btrfs la misma copia da los 32 MiB exactos: es un rasgo de XFS, no de las herramientas.

tar sin -S infla también el archivo .tar. Produce uno de 1,1 GiB en lugar de los 33 MiB que ocupa con -S. Ese gigabyte de ceros viaja por la red, se escribe en la cinta o en el bucket, y recién se nota cuando el job de backup llena el volumen de destino.

Con gzip, bzip2 o xz la falla es más difícil de ver, porque un gigabyte de ceros comprime a casi nada. El .gz es minúsculo y todo parece correcto. El gunzip en el destino escribe el gigabyte completo, sin agujeros.

De las herramientas de transferencia, scp y sftp no tienen ninguna noción de agujeros. Tampoco git: el blob incluye todos los ceros. dd los re-infla salvo que le pases conv=sparse.

La regla que se deduce de la tabla

Si una herramienta ofrece un flag explícito de sparse —rsync -S, tar -S, dd conv=sparse, cp --sparse=always, restic restore --sparse, borg extract --sparse— es señal de que por defecto no lo hace, o de que su default es una heurística que conviene verificar. Las que ni siquiera ofrecen la opción re-inflan siempre.

En backup empresarial el patrón se repite: Bacula y Bareos manejan agujeros solo si activás la directiva Sparse = yes, que viene desactivada. restic y borg deduplican los ceros muy bien —el repositorio queda chico— pero la restauración los re-infla si no le pedís lo contrario.

Hay una excepción importante, para no exagerar el problema: el backup a nivel de imagen o de bloque no lee archivos, lee bloques asignados del dispositivo. Los agujeros no le representan un problema. El riesgo descrito acá aplica al backup y la restauración a nivel de archivo.

El origen nunca se queja: el espacio real de un sparse file es minúsculo. La medición que importa no es la del archivo que tenés, sino la del que vas a producir —el destino de la copia, el .tar que se sube, el volumen donde se restaura—. Un du -h contra un du -h --apparent-size en el origen te dice cuánto puede crecer; los flags de la herramienta te dicen si va a crecer.

Fuentes y más recursos

  • stat(3type) — define los campos de struct stat. Sobre st_blocks: "the number of blocks allocated to the file, in 512-byte units". El stat(2) advierte que esa unidad no es universal (HP-UX cuenta en 1024, AIX en 4K); en Linux es 512.
  • lseek(2) — define SEEK_HOLE y SEEK_DATA, el mecanismo que permite recorrer agujeros sin leer el archivo entero. Disponibles desde Linux 3.1.
  • fallocate(1)--punch-hole para perforar un rango y --dig-holes para convertir en agujeros las secuencias de ceros ya escritas.
  • filefrag(8) — inspecciona el mapa de extents de un archivo; viene en e2fsprogs.
  • man 1 cp — la sección de --sparse aclara que la detección por defecto (auto) usa "un mecanismo heurístico muy básico": por eso conviene medir en lugar de asumir.
  • man 1 tar-S/--sparse y --hole-detection=seek|raw, que usa SEEK_HOLE cuando puede y cae a leer el archivo crudo cuando no.
  • Restauración con restic — documenta que "By default, restic does not restore files as sparse"; hay que pasar --sparse.
  • borg extract — su opción --sparse recrea los agujeros a partir de los bloques de ceros.

La serie

Parte Post De qué trata
1 Archivos sparse en Linux (estás acá) Los agujeros: cómo un archivo declara un tamaño y ocupa otro
2 Particularidades de lastlog El UID como posición dentro del archivo, y todo lo que se desprende de esa decisión
3 Una máquina de 64 bits no es inmune al 2038 El campo de fecha de 32 bits, lo que hizo glibc y el reemplazo

  1. Ese mismo espacio asignado lo expresa cada herramienta en su propia unidad, y los números no siempre coinciden: stat da st_blocks = 136 (136 × 512 = 69632), ls -ls pone 68 en su primera columna (68 × 1024 = 69632) y du dice 68K. La diferencia entre 136 y 68 es solo la unidad —512 bytes contra 1024, el default de GNU ls—. Con POSIXLY_CORRECT, o con ls -s --block-size=512, ls -s baja a 512 y coincide con st_blocks

  2. En btrfs con compresión (el default de Fedora) esas cifras de 1 GiB salen mucho más chicas: los ceros reescritos se comprimen a casi nada. El archivo se re-infla igual en el espacio lógico, pero el filesystem disimula el costo en disco. 

Intel y AMD: quién fija el límite de potencia del CPU

Hace unas semanas conté cómo limité térmicamente una notebook Intel usada como CPU box en clamshell —tapa cerrada, monitor portátil encima, la máquina debajo de todo—: Gestión térmica en notebook clamshell con RAPL y tuned. La idea era bajar a mano el techo de potencia para que el CPU no entrara en throttling (que no se frenara solo al recalentarse).

Cuando fui a repetir lo mismo en un equipo AMD Ryzen, el ejercicio derivó en algo más interesante que portar una receta. Intel y AMD no se diferencian solo en cómo se toca el límite: se diferencian en quién puede fijar el límite de la potencia y la frecuencia — el hardware solo, o también vos. Y esa diferencia, no el clamshell, es lo que vale la pena contar.

El clamshell quedó como lo que era: el escenario que me obligó a medir. Lo uso de banco de pruebas, pero el tema es la comparación.

Cómo controla Intel: el límite lo escribís vos

En Intel, el techo de potencia es algo que escribís vos. RAPL (Running Average Power Limit) expone dos límites de potencia: PL1, el techo sostenido que el chip mantiene en el tiempo, y PL2, uno más alto que se permite por ráfagas cortas (el boost). Viven en el powercap framework del kernel —la interfaz bajo /sys/class/powercap para poner topes de potencia— y son de lectura/escritura:

echo 12000000 | sudo tee /sys/class/powercap/intel-rapl:0/constraint_0_power_limit_uw

Ese echo baja la potencia disponible y el PCU (Power Control Unit) del CPU la respeta. El hardware sigue teniendo su propia gestión térmica, pero ese tope es un control compartido: el sysadmin puede ponerlo por debajo del default de fábrica, y el procesador lo aplica.

Cómo controla AMD: el límite lo decide el firmware

En AMD ese control, sencillamente, no te lo dan. La rama de powercap puede existir —los Ryzen modernos exponen RAPL— pero con una diferencia decisiva:

En AMD, el RAPL del powercap reporta energía consumida (energy_uj), pero los archivos de límite de potencia (constraint_*_power_limit_uw) no se exponen. El máximo de potencia no se fija desde el sistema operativo.

No es un problema de permisos: el kernel no expone esa escritura para AMD por ninguna vía mainline (lo que viene de fábrica en el kernel oficial). Y hasta la lectura de energía está acotada: el energy_uj que aparece más abajo es legible solo por root desde el kernel 5.10, como mitigación de un ataque de canal lateral (PLATYPUS). Escritura del límite, en cambio, nunca hubo. El gobierno de la potencia y la frecuencia ocurre adentro del SMU (System Management Unit), el microcontrolador del SoC, que regula solo, en tiempo real, contra el primero de varios límites que se sature —potencia, corriente y temperatura—. El sistema operativo no hace esa regulación fina ni escribe el límite de potencia, pero tampoco es un espectador pasivo: le fija la política dentro de la cual optimizar —el gobernador, la preferencia de eficiencia o rendimiento (EPP) y el perfil de energía— y lee la telemetría.

Comprobalo en tu propia máquina. En vez de apuntar a un archivo puntual, listá el contenido de la zona del paquete y mirá qué hay:

cat /sys/class/powercap/intel-rapl:0/name
ls -l /sys/class/powercap/intel-rapl:0/

Dos cosas para notar. La primera: el dominio se llama intel-rapl aun en una máquina AMD. No es un error — el soporte RAPL de AMD se registra reusando el mismo framework y el mismo nombre que ya existía en el kernel. La segunda, y es el punto: en mi equipo Ryzen el name da package-0 y adentro solo hay energy_uj (el contador de energía consumida, de lectura). No existe ningún archivo constraint_* — ni constraint_0_power_limit_uw ni sus variantes. En un Intel, esa misma carpeta tendría los constraint_0_* (PL1) y constraint_1_* (PL2): los archivos donde se escribe el techo de potencia. En AMD el concepto de límite ni se expone. RAPL mide; no limita. Y es así por cómo está implementado el driver de AMD en mainline, no por una particularidad de este equipo —aunque no probé cada combinación de kernel y BIOS.

La evidencia: qué hace el firmware de AMD bajo carga

Medí el equipo AMD en clamshell y en AC —la condición real de un CPU box: tapa cerrada, en corriente, con un monitor de 14" apoyado encima bloqueando el aire— con un baseline en reposo y un stress de 20 minutos sostenidos (stress-ng --cpu 0), registrando temperatura de paquete (Tctl, el sensor que reporta AMD vía k10temp) y frecuencia.

En reposo, ~40 °C. Bajo carga, la temperatura sube despacio y se asienta cerca de 73 °C —todavía trepando muy de a poco al cabo de los 20 minutos—, mientras la frecuencia se mantiene alrededor de ~3,0 GHz toda la corrida, sin caídas abruptas:

22:59:54  temp=59.2°C  freq=3044.129MHz
23:05:30  temp=69.0°C  freq=3019.180MHz
23:10:25  temp=71.2°C  freq=3019.166MHz
23:19:42  temp=73.4°C  freq=3019.182MHz

Dos cosas para leer de ahí:

  1. El firmware sostiene un punto de operación estable de ~3,0 GHz con todos los núcleos cargados, sin que nadie intervenga. No es un techo que le puse yo: scaling_max_freq estaba en el máximo de fábrica. Es el SMU manteniendo la frecuencia que la potencia disponible le permite sostener.
  2. La temperatura se queda lejos de cualquier límite. 73 °C, lejos de su límite térmico —el Tjmax: la temperatura a la que el chip se frena o se apaga para no dañarse, del orden de 90–100 °C según el modelo de Ryzen—: margen de sobra. Y en 20 minutos no hubo una sola caída abrupta de frecuencia: el firmware regula y se queda quieto.

Qué significa la diferencia

Dos formas de repartir el control:

  • En Intel el control se reparte. El PCU regula, pero te deja escribir el techo de potencia en mainline (PL1/PL2): podés bajar el límite por debajo del default de fábrica y el procesador lo respeta. Queda compartido entre el hardware y vos. En el post Intel fijé PL1 bajo y tuned activo, y la frecuencia se sostuvo estable en clamshell — aunque, en realidad, apliqué las dos cosas juntas y no aislé cuánto aportó cada una.
  • En AMD el control queda en el firmware. El SMU regula de forma autónoma; el sistema operativo le marca una preferencia —hacia eficiencia o rendimiento, vía EPP y el perfil de energía— pero no le escribe el límite de potencia. Para ponerle un techo duro desde mainline, lo único que queda es la frecuencia (scaling_max_freq). Pero —y esto es lo que muestran las mediciones— el firmware ya está haciendo la regulación: en 20 minutos de carga sostuvo la frecuencia estable por su cuenta, sin que yo tocara nada.

De esto se sigue algo que va a contramano del reflejo de optimizar:

En este equipo AMD, bajar el techo de frecuencia no solo es innecesario (el firmware ya regula solo) — es contraproducente: le quitarías rendimiento que el chip podía sostener, sin ganar nada en seguridad térmica.

La receta de RAPL no se porta a AMD, y no solo porque la API sea distinta (el powercap de AMD solo expone telemetría, sin archivos de límite). Y el problema que la justificaba, en este hardware, tampoco existe: el firmware moderno de AMD gobierna el clamshell solo.

Si igual querés meter mano en AMD

Para cerrar, las dos vías reales y su estado:

  • Techo de frecuencia (scaling_max_freq, mainline). Es lo que el kernel te da, sin agregar nada, para ponerle un techo duro: un echo por core, persistible al boot (un profile de tuned o un service propio), sin tocar voltaje. Útil si tu equipo sí oscila o si querés sacrificar rendimiento a propósito por un equipo más fresco. Por lo que vimos, para seguridad térmica acá sobra.
  • El tope de potencia (PPT vía ryzenadj). Es la forma de llegar a ese tope del SMU desde el sistema —los límites que AMD llama PPT (Package Power Tracking, el tope de potencia del paquete) y STAPM (atado a la temperatura de la carcasa)— pero queda fuera de mainline: ryzenadj (LGPL-3.0) se compila a mano y necesita el módulo ryzen_smu (GPL-2.0), que vive out-of-tree —fuera del árbol del kernel— y hay que recompilar en cada actualización (eso lo automatiza DKMS), además de pelear con Secure Boot. Es libre y se usa en producción en handhelds (Bazzite, Steam Deck), pero es tooling de entusiasta, no de infraestructura. Y es justamente la prueba de la tesis: el control que Intel expone en mainline, en AMD hay que buscarlo fuera del árbol. Nada de esto es peligroso —es runtime, no flashea firmware, un reboot revierte todo— pero para un CPU box que tiene que comportarse como infraestructura, el costo no se justifica.

Contrastar la premisa y después medir

El reflejo sería asumir que el setup Intel se porta cambiando una ruta de /sys. Pero la premisa hay que contrastarla, y no es la misma en los dos fabricantes: en AMD el archivo para establecer un límite ni siquiera está. Contrastarla a tiempo es lo que evita el desvío siguiente —buscar la herramienta que sí escribe watts y terminar en ryzenadj con un módulo out-of-tree—. Y antes de salir a buscar esa herramienta, conviene medir: acá la medición mostró que no había nada que arreglar.

La diferencia de arquitectura entre Intel y AMD define si te toca intervenir o si el firmware ya lo hace.

Lo que me llevo

  • Intel y AMD difieren en quién escribe el límite de potencia: Intel te lo deja en mainline (RAPL r/w); AMD lo retiene en el SMU —desde el SO le marcás una preferencia (eficiencia o rendimiento) y leés energía, pero el tope no lo escribís—.
  • En AMD, el firmware sostiene una frecuencia estable por su cuenta (~3,0 GHz en 20 min de carga) y la temperatura se queda con margen de sobra respecto del Tjmax. Sin oscilación ni throttling.
  • En AMD el tope de potencia (PPT) no se escribe en mainline —solo con tooling out-of-tree (ryzenadj)—; en mainline tenés la política (EPP, perfil de energía) y un techo por la frecuencia (scaling_max_freq), pero no el límite de watts directo. Es la contracara exacta de lo que Intel expone de fábrica.
  • Antes de optimizar, medir: en este equipo, bajar el techo habría sido contraproducente.
  • tuned cruza sin cambios entre plataformas — solo cambia el driver de scaling debajo (amd_pstate en vez de intel_pstate).

Fuentes

Documentación oficial y mediciones en el propio equipo.

Documentación oficial

Verificado en el equipo

  • Que el powercap de AMD solo expone energy_uj y ningún constraint_*, y la jerarquía package-0/core: los comandos cat/ls sobre /sys/class/powercap/ que están en el cuerpo. El driver (amd-pstate-epp) y las palancas de política (EPP, platform_profile) se leen read-only de /sys/devices/system/cpu/cpufreq/ y /sys/firmware/acpi/.
  • Que el firmware sostiene la frecuencia sin intervención: baseline y stress de 20 minutos, reproducibles con stress-ng --cpu 0 y la lectura de Tctl (k10temp).

3 Power Tips + Power Link I12

"GUI vs CLI" es un falso dilema. Antes que técnico, es cultural: hábitos, modos de trabajo y supersticiones que arrastramos quienes trabajamos en IT. La consola y la interfaz gráfica no se disputan el mismo lugar; resuelven problemas distintos y, bien usadas, se complementan.

Power Tip #1 Una imagen contra una tabla de sectores

Hay que redimensionar una partición con datos, y querés ver cómo queda el layout antes de escribir al disco.

La línea de comandos no te deja sin red: sgdisk o un playbook de Ansible pueden simular el cambio sin escribir al disco. El tema es cómo lo muestran: una tabla de números —alineación a sectores, espacio libre real, orden físico de las particiones— que hay que reconstruir de cabeza.

GParted o KDE Partition Manager muestran ese mismo estado final, pero dibujado:

gparted /dev/sdX

Cargan el layout, dejan encolar varias operaciones, y muestran el resultado antes de aplicarlo. El "Aplicar" recién dispara las llamadas reales al disco.

Barra de herramientas del Gestor de Particiones de KDE con «Aplicar» y «Cambiar el tamaño/mover», y la lista de particiones del disco: los cambios se preparan sobre el layout y nada se escribe hasta pulsar «Aplicar»

Cuando la operación es difícilmente reversible y conviene evaluar el estado de antemano, la GUI es un camino apropiado: no porque la CLI no pueda simular, sino porque entrega una imagen donde la consola entrega una tabla. Y eso no tiene que ver con el nivel del operador.

Power Tip #2 El clic no se copia de manera natural

Ajustaste una regla de firewall desde Cockpit, el servicio volvió, y treinta días después hay que replicarlo en otros 50 nodos.

  • El cambio se hizo en la web UI, no en una shell.
history | grep firewall-cmd

No aparece nada: no hay un comando para copiar.

firewall-cmd --permanent --add-service=https && firewall-cmd --reload

La CLI permite reproducir y adaptar; la GUI facilita hacerlo una sola vez.

Power Tip #3 Un comando contra el asistente de media docena de pantallas

Hay que crear el mismo private endpoint en dev, staging y prod, y en el Portal es un asistente de varias pantallas cada vez.

  • Resource group, región, sub-resource, VNet, subnet: cada pantalla es un clic y un punto donde el tercer entorno termina distinto del primero.
  • No hay forma de correr "lo mismo" dos veces: cada pasada se arma a mano de nuevo.
az network private-endpoint create --name pe-app --resource-group rg-prod \
  --vnet-name vnet-prod --subnet snet-data \
  --private-connection-resource-id "$sqlid" --group-id sqlServer \
  --connection-name pe-app-conn

La misma operación entra en una línea parametrizable: la repetís idéntica en cada entorno cambiando un par de variables. El patrón no es de Azure — aws y gcloud resuelven lo suyo igual.

Cuando la misma operación se repite entre entornos, el asistente visual es el cuello de botella, no la ayuda.

"La IA provocará la inutilidad de la línea de comandos". ¿Será así? Mark Pesce —co-creador de VRML— sostiene en The Register justo lo contrario: AI has made the Command Line Interface more important and powerful than ever before. Su argumento es que las GUIs se volvieron tan recargadas que ni los agentes las operan bien, y la CLI reaparece como el terreno común entre humanos y máquinas. No hace falta estar de acuerdo en todo para que la pregunta quede flotando: ¿cuánto de lo que se grita en los medios y en las redes sociales vamos a tener que repensar?

Gestión térmica en notebook clamshell con RAPL y tuned

Tapa cerrada, monitor portátil apoyado encima, notebook como CPU box debajo de todo: lo que se llama modo clamshell —la notebook cerrada como una almeja, usada solo como unidad de cómputo con monitor externo—. El objetivo: evitar que suba la temperatura dañando el hardware.

El problema, brevemente

Los notebooks toman aire por abajo y/o expulsan calor por arriba del teclado. Un monitor portátil apoyado en clamshell rompe ese flujo. Como no se puede recuperar la disipación sin tocar hardware, lo que queda es bajar voluntariamente el techo de potencia para que el CPU jamás llegue a temperaturas de throttling.

Mejor 12W tibios sostenidos que un loop entre 28W ardiendo y 5W throttleado.

Eso es todo. El resto del post es la pelea para implementarlo en Fedora 43 (el setup no cambió en Fedora 44).

Las tres palancas

Linux expone tres puntos donde tocar, y cada uno opera en un plano distinto del sistema:

RAPL (Running Average Power Limit). El CPU Intel se autoimpone límites de potencia: PL1 sostenido, PL2 boost. Están en /sys/class/powercap/intel-rapl/. Si los bajás, el CPU físicamente no puede consumir más. Es el techo duro, aplicado por el hardware del procesador (el PCU, Power Control Unit): no depende ni del scheduler ni del usuario.

tuned. Daemon de gestión de perfiles de energía. Desde Fedora 41 es el default del sistema (vía tuned-ppd), reemplazando a power-profiles-daemon. Maneja governor, EPP, runtime PM, sysctls, sysfs. No baja literalmente el consumo: configura los subsistemas del kernel para que ellos lo hagan. Opera en el plano de las políticas, no del hardware — por eso no se solapa con RAPL.

thermald. Reactivo: lee zonas térmicas y aplica medidas correctivas cuando se calienta. En este equipo nunca funcionó, y de eso también va el post.

RAPL, primer round

Bajar PL1 a 12W y PL2 a 20W es un echo a un archivo en /sys/:

echo 12000000 | sudo tee /sys/class/powercap/intel-rapl:0/constraint_0_power_limit_uw

Tres segundos, listo. Después de verificar que el valor quedó aplicado y que las temperaturas en stress test caían a una zona razonable, vino la primera trampa con la que choco cada tanto y siempre me tomo unos minutos para acordarme:

/sys no persiste. Lo que escribís ahí desaparece al reboot.

Solución: un service unit propio (rapl-limits.service) que reaplica los valores al boot. Heredoc, daemon-reload, enable --now. Hecho.

thermald, el daemon zombi

Acá viene la parte que me costó descubrir.

Habilito thermald, escribo un /etc/thermald/thermal-conf.xml custom, lo arranco. systemctl status lo reporta activo. ps -ef | grep thermald muestra el proceso vivo. Todo bien.

Solo que no estaba haciendo absolutamente nada.

Días después, ya con el sistema funcionando bien y con thermald deshabilitado por sospechoso, abro journalctl -u thermald para confirmar la sospecha. Encuentro esto, mezclado entre los logs:

thermald[1010]: Thermal DTS: No coretemp sysfs found
thermald[1010]: Thermal DTS or hwmon: No Zones present Need to configure manually
thermald[1010]: XML zone: invalid sensor type x86_pkg_temp
thermald[1010]: Zone update failed: unable to bind

Una sola causa raíz, dos síntomas en el log:

  1. No había un sensor térmico del CPU que descubrir. El módulo que expone la temperatura de paquete no estaba cargado (No coretemp sysfs found), así que thermald arrancó sin ninguna zona del CPU a la vista. Lo venía diciendo desde el primer boot del histórico, mucho antes de que yo empezara a tocar nada.
  2. El invalid sensor type x86_pkg_temp no significa lo que parece. x86_pkg_temp es el nombre correcto del sensor de paquete; thermald no lo rechaza por schema. Resuelve el tipo en runtime contra los sensores que efectivamente descubrió, y como no había ninguno, la búsqueda volvió vacía y el bind falló. El mensaje dice "invalid type" pero quiere decir "no encontré un sensor de ese tipo en este sistema".

Resultado: el daemon arrancaba, intentaba bindear las zonas definidas en el XML, no encontraba el sensor, y se quedaba corriendo sin nada que monitorear. Vivo, ocupando memoria, sin función.

systemctl status te dice si systemd cree que el servicio está bien. No te dice si el servicio está haciendo lo que tiene que hacer. Para eso está journalctl.

Validación empírica

Antes de tomar la decisión final de desactivar thermald, corrí un baseline de 6 minutos midiendo Package temp y frecuencia, después un stress test de 6 minutos exactos midiendo lo mismo, y los comparé. Lo que vi fue lo que esperaba: con RAPL fijo en 12W y tuned manejando el resto en su profile balanced, las temperaturas se mantenían en una zona estable y la frecuencia se sostenía sin las caídas abruptas del clamshell sin tunear.

La pregunta que la medición respondía no era "¿thermald aporta?" — eso ya estaba contestado de antemano por los logs. Era la otra: "¿con solo RAPL+tuned alcanza para clamshell?". La respuesta fue sí, y eso permitió cerrar el setup con tranquilidad.

Estado final

tuned                 → active (profile balanced)
RAPL PL1=12W PL2=20W  → fijo vía rapl-limits.service
thermald              → disabled (no podía operar en este hardware)

Filosofía: un techo duro de potencia (RAPL), un orquestador de estado (tuned), nada reactivo encima. Tres piezas, ninguna pelea con la otra, todas debuggeables.

Una nota sobre cómo empecé

Originalmente armé el setup con tlp + RAPL, siguiendo una sugerencia de Claude (vía claude.ai) que asumía power-profiles-daemon como el daemon por defecto en Fedora. No contrasté la premisa. Cuando fui a verificarla, descubrí que desde Fedora 41 el default pasó a tuned (vía tuned-ppd), no PPD. Y para este escenario — clamshell estático, siempre AC, sin diferenciar AC/BAT, sin que tlp-rdw aporte nada — tlp no resolvía nada que tuned no resuelva. Migré. El post documenta el setup resultante, no el camino original.

La lección no es "no preguntarle a una IA". Es verificar la premisa que la IA está usando, no solo la recomendación que te entrega. Un asistente puede recomendar una herramienta correctamente condicional a un default que ya no es el actual; sin contrastar esa premisa, terminás con un setup más complicado del necesario.

Lo que me llevo

  • Un daemon puede estar "running" y no estar haciendo nada. La verdad está en los logs de aplicación, no en systemctl status.
  • Un unit file propio de pocas líneas a veces es la decisión correcta y que no requiere demasiado esfuerzo.
  • El consejo técnico de una IA puede ser internamente coherente y aún así basarse en premisas obsoletas (defaults, versiones, comandos). El costo de chequear la premisa antes de implementar es bajo; el costo de no hacerlo se paga cada vez que volvés a tocar el equipo.

3 Power Tips + Power Link I11

Las PTs de este issue muestran cómo, incluso en la nube (en este caso Azure), siguen aplicando conceptos clásicos de sistemas: distinguir entre ejecución y estado, entender la irreversibilidad de ciertos valores (como secrets) y operar en ausencia de un mapeo explícito entre componentes. Al final, el progreso en la Estrategia de Innovación Abierta y Código Abierto en un estado alemán.

Power Tip #1 El exit code es local, no de Azure

El comando volvió con 0 y la app sigue cayéndose.

  • az reporta éxito al recibir el ACK del control plane.
  • El recurso queda en Updating o Creating por minutos.
  • El siguiente paso del script lo encuentra "no listo" y falla raro.
az resource show --ids "$id" --query "properties.provisioningState" -o tsv

provisioningState es un testigo más confiable del control plane.


Power Tip #2 El valor del secret se entrega una sola vez

La alerta de vencimiento de un secret se "resolvió" pero la app igual se cae.

  • credential reset devolvió el password en stdout.
  • Lo perdiste antes de guardarlo en el vault.
  • Entra ID considera el secret válido. Nadie puede usarlo.
az ad app credential reset --append ... --query password -o tsv

Si se perdió el valor generado, no se puede recuperar. Como perder una clave privada SSH: podés crear otra, pero no reconstruir la anterior.


Power Tip #3 El mapeo no vive donde uno espera

Sabés que la app usa un secret de un KV. No sabés cuál.

  • App Registration no declara qué KV la consume.
  • Key Vault no mantiene, por defecto, una relación nativa con la App Registration que originó el secret.
  • El nombre del secret en KV es convención del equipo responsable, no metadato de Azure.
for kv in $(az keyvault list --query "[].name" -o tsv); do
  az keyvault secret list --vault-name "$kv" \
    --query "[?contains(name, 'mi-superpower-app')].{kv:'$kv',name:name}" -o tsv
done

Si para rotar un secret tenés que salir a buscarlo, no está definido en ningún lado qué secret usa esa app...

Este es un análisis del Open Source Observatory (OSOR) sobre la adopción de software Open Source en el estado alemán de Schleswig-Holstein. ¿Cómo les fue implementando LibreOffice, Thunderbird, and Linux?: From Early Adopter to Leader: Schleswig-Holstein's Open Source Evolution

3 Power Tips + Power Link I10

Las herramientas de monitoreo son fundamentales, pero una pobre interpretación de los resultados, pueden llevar a conclusiones incorrectas. Incluso usando chatbots de IA, si hacemos preguntas deficientes podemos terminar girando en círculos o tardar más tiempo en resolver un problema. Lo mejor es reducir el nivel de especulación con hipótesis técnica. A continuación vemos unos ejemplos bien sencillos.

Power Tip #1

No todas las métricas que parecen similares describen el mismo fenómeno.

  • Aplicación lenta.
  • Load 15.
  • CPU idle 50%.

Alguien descarta CPU porque “está libre”.

Pero:

  • Load describe tareas en ejecución o esperando.
  • CPU usage describe tiempo ocupado.
  • CPU pressure describe tiempo en que las tareas no pudieron progresar.

Son fenómenos distintos.

Antes de interpretar, mirá comportamiento:

vmstat 1 5
mpstat 1

Y si necesitás otra capa:

cat /proc/pressure/cpu

Es importante saber que “Load no es necesariamente consumo de CPU”, sin embargo, más importante es entender qué está describiendo cada señal.

Interpretarlas como si hablaran de lo mismo es especulación con números.

Power Tip #2

Tomar una métrica aislada de memoria en Linux no implica entender como funciona

free -h

Las columnas están ahí. Pero entender qué representa cada una es otra cosa.

  • used no significa “memoria en crisis”.
  • buff/cache no significa “desperdicio”.
  • available no significa “memoria libre inmediata”.

Antes de reaccionar ante un número alto, mirá dinámica:

vmstat 1 5

Linux administra memoria como recurso dinámico, no como espacio estático.

Leer columnas sin entender el modelo del kernel es especulación con formato tabular.

Power Tip #3

Reiniciar en muchas situaciones no hace otra cosa que restaurar un estado. No explica causas.

  • El servicio falla.
  • Se reinicia.
  • Funciona.

Eso no es diagnóstico.

Antes de repetir el gesto automático:

journalctl -u servicio-problematico -n 50

Si no entendemos por qué se degradó, volverá a pasar.

Reiniciar elimina el síntoma. No valida la hipótesis. Las herramientas no están equivocadas.

La diferencia está en entender qué fenómeno describe cada señal. Está claro que en el mundo real, muchas veces determinar la causa del problema lleva más tiempo que usar un trigger que reinice un servicio. Algo que pasa muchas veces apps legacy (la falta de especialistas o la triste realidad de falta del código fuente). Sin embargo, el problema es cuando se usa esta metodologìa como primera opción...

La IA terminará con las operaciones de IT. ¿En serio? Bueno, en realidad no, para sorpresa de tecno-optimistas o tecno-pesimistas, aquí hay un interesante análisis de Sebastián Martínez de SUSE sobre este tema: Why AI Still Cannot Run Your Linux Infrastructure (And What Must Change)

3 Power Tips + 1 Power Link I9

PSI (Pressure Stall Information) permite observar ese tipo de situaciones y, sobre todo, cambiar la forma en que interpretamos problemas de rendimiento y estabilidad en Linux. A partir de esa señal, el foco deja de estar en métricas globales y pasa a decisiones concretas: entender dónde ocurre el problema, a quién afecta y cómo intervenir sin arrastrar todo el sistema. Finalmente, el Power Link nos lleva a ver qué opina Gaël Duval, el creador de la distribución Mandrake, sobre uno de los hypes actuales.

Power Tip #1: ¿Cómo se ven afectadas mis aplicaciones por falta de recursos?

Existe a veces una creencia basado en pensamiento mágico: que el monitoreo depende solamente de la herramienta que se utilice. Pero más que la herramienta lo importante, es qué se mide y cómo interpretar esos resultados. Las métricas más conocidas: uso de cpu, load-average, uso de memoria, uso de I/O, etc. si bien son útiles pueden darnos en muchos casos un panorama parcial. Eso sucede porque están basados en los recursos, o en el scheduler.

Existen sin embargo desde hace varios años métricas que están más centradas en como las aplicaciones se ven impactadas por el uso de recursos. Con PSI (Pressure Stall Information) obtenemos un indicio del tiempo que las aplicaciones quedan detenidas porque el sistema no puede darles CPU, memoria o acceso a disco en el momento en que lo necesitan.

Esto está expuesto en el sistema de archivos:

Archivos de PSI

PSI distingue entre situaciones donde al menos una tarea queda bloqueada (some) y escenarios donde todas las tareas relevantes compiten por el mismo recurso (full). Valores elevados y sostenidos en este full caso suelen indicar un problema estructural: no de picos puntuales, sino de diseño, aislamiento o priorización de workloads.

Vale hacer aquí tres aclaraciones:

  1. Esta funcionalidad está presente a partir de la versión 4.20 del kernel (RHEL8 la trae como backport). En algunas distribuciones puede ser necesario habilitar PSI explícitamente en el arranque.
  2. Esta no es la métrica definitiva, sin embargo ignorarla es perder el cuadro completo de lo que sucede tanto en el sistema como nuestras aplicaciones.
  3. Versiones recientes del kernel incluyen también la medición de la presión sobre irq.

Power Tip #2: Las disrupciones se pueden atribuir a workloads específicos

Cuando el host empieza a perder capacidad de respuesta, no siempre el problema es global. Es común que el sistema siga siendo usable mientras algunos servicios o aplicaciones comienzan a degradarse, simplemente porque no acceden a los recursos cuando los necesitan. Esa degradación suele manifestarse como lentitud, mayor latencia o pérdida de capacidad de interacción.

En un Linux moderno, el kernel organiza los procesos bajo dominios de control definidos por cgroups. Esto permite observar de forma localizada qué tareas empiezan a quedarse esperando recursos, en lugar de razonar únicamente a partir de métricas agregadas del host.

En sistemas basados en systemd, ese dominio no es abstracto: se corresponde con services, slices o scopes. El mismo síntoma que aparece a nivel global puede observarse dentro de estos límites, lo que permite atribuir dónde se generan las esperas, incluso cuando el resto del sistema continúa funcionando con normalidad.

Pensar a nivel host diluye el diagnóstico. En cambio poner el foco en en services, scopes y slices permite relacionar directamente los síntomas percibidos —lentitud, tiempos de respuesta, degradación de rendimiento— con un grupo de procesos concreto.

PSI per cgroup

Es decir, uno puede decir que el host muestra la presión acumulada, pero los cgroups permiten entender qué se queda atascado y frente qué recurso.

Power Tip #3: Abordar el problema de manera localizada

Cuando un servicio o aplicación empieza a tener tiempos muertos o a interferir con el resto, tenemos varias maneras de abordar el problema. Y no estamos hablando de las típicas acciones: reiniciarlo, moverlo o rediseñarlo.

Podemos tomar medidas más estructurales, por ejemplo, crear un slice de systemd, establecer límites y luego confinar esos servicios o procesos en esos slices.

Sin embargo, antes de intervenir de manera prematura, es conveniente tener en cuenta que Linux, en general ya tiene una agrupación de slices, scopes y servicios bastante razonable. En este punto entonces, ya sabemos que ese servicio problemático ya corre dentro de un perímetro definido.

En lugar de crear un nuevo slice, que tiene un carácter más permanente, podemos adoptar una solución más sencilla y gradualista. En systemd, los parámetros que gobiernan cómo compite por recursos no están fijos en scopes o servicios.

Un vistazo rápido alcanza para verlo (los siguientes son ejemplos para entender la idea):

systemctl show -p ControlGroup,CPUWeight,MemoryMax,IOWeight procesos-ruidosos.scope

Y tomar cartas en el asunto:

systemctl set-property procesos-ruidosos.scope 

Ese tipo de intervención es local, reversible y no convierte un incidente puntual en un cambio estructural.

Intervenir sobre una unidad existente permite corregir interferencias concretas sin rediseñar el sistema ni propagar el problema.

Fuentes y más recursos

Qué son los cgroups y para qué sirven

Gaël Duval, creador de Mandrake, Murena y e/OS reflexiona sobre el impacto de la AI en el software open source: Why AI won’t “Kill Open Source”