wandres.dev
PHD: MANTENER Y EL FUTURO · ser maintainer, el futuro

La regla sagrada: no romper el espacio de usuario

La primera ley del desarrollo del kernel según Torvalds: si un programa funcionaba, debe seguir funcionando. Por qué la ABI hacia userspace está congelada para siempre mientras el código interno cambia sin ninguna promesa de estabilidad, cómo se diseña una syscall para que pueda crecer sin romperse, y por qué esta regla, más que técnica, es un pacto moral con todo el mundo del cómputo.

⏱ 15 min

De todas las reglas del kernel, una está por encima de las demás, y Torvalds la escribe en mayúsculas cuando alguien la olvida: no rompemos el espacio de usuario. Si un programa funcionaba en un kernel viejo, tiene que seguir funcionando en el nuevo, aunque su autor lleve veinte años sin tocarlo, aunque dependa de un comportamiento que nunca debió existir. El kernel sirve a userspace, no al revés. Esta lección diseca esa asimetría radical —libertad total dentro, congelación absoluta en la frontera— y por qué es, probablemente, la razón por la que Linux ganó.

🎯 Al terminar esta lección sabrás
  • Enunciar con precisión la regla y qué superficie exacta protege.
  • Entender la asimetría: ABI externa congelada, API interna sin ninguna promesa.
  • Diseñar una interfaz a userspace que pueda crecer sin romper lo antiguo.
  • Reconocer el pacto moral que se esconde tras una regla de ingeniería.

La regla y su superficie

La regla dice: si un cambio en el kernel hace que un programa de usuario deje de funcionar, es un fallo del kernel. No del programa. Aunque el programa hiciera algo raro, aunque se apoyara en un detalle no documentado, aunque su forma de usar el kernel fuera fea. El veredicto es inmediato y no admite apelación: se revierte el cambio del kernel, por correcto que fuera, y punto.

¿Qué superficie protege exactamente? Todo lo observable desde el otro lado de la frontera: las llamadas al sistema y sus argumentos, el contenido y formato de /proc y /sys, los ioctl, los códigos de error, e incluso comportamientos que nadie documentó pero de los que algún programa acabó dependiendo. Esa superficie es la ABI hacia userspace, y una vez que un binario la usa en libertad, queda congelada para siempre.

⚠️
No todo lo visible es ABI

Hay una excepción deliberada: debugfs y buena parte de tracefs se declaran explícitamente inestables. Son ventanas de depuración que pueden cambiar entre versiones sin aviso, precisamente para no congelar por accidente cada detalle interno que se asoma a ellas. La regla protege los contratos, no los miradores. El árbol documenta qué es estable en Documentation/ABI/, clasificado en stable, testing y obsolete.

La asimetría: dentro todo cambia, fuera nada

Aquí está lo que descoloca a quien viene de otros mundos. Mientras la ABI externa es intocable, no existe ninguna API interna estable. El kernel presume de ello en un documento célebre, stable-api-nonsense.rst: las estructuras internas, las firmas de las funciones, los cerrojos, los asignadores… todo puede reescribirse en cualquier release. Y a lo largo de este track lo has visto ocurrir: SLAB y SLOB eliminados, el planificador sustituido por EEVDF, las VMAs migradas a un maple tree, struct page cediendo el paso al folio. Nada de eso rompió un solo programa de usuario, porque nada de eso cruzaba la frontera.

flowchart TD
APP[Programas de usuario, binarios de hace decadas] --> ABI[Frontera ABI: syscalls, proc, sys, ioctls]
ABI --> IMPL[Implementacion interna]
IMPL --> C1[Planificador reescrito: CFS a EEVDF]
IMPL --> C2[Asignador reemplazado: fuera SLAB y SLOB]
IMPL --> C3[Estructuras mutando: page a folio, maple tree]
style ABI fill:#f9e2af,color:#11111b
style APP fill:#a6e3a1,color:#11111b

La asimetría no es contradictoria: es causal. La estabilidad de la frontera es precisamente lo que compra la libertad de demoler todo lo de dentro. Como ningún programa puede depender de cómo está implementado el planificador —solo de que sched_setattr siga comportándose igual—, el planificador puede reescribirse entero. Congela una línea, y podrás rehacer sin miedo todo lo que quede a ambos lados de ella.

Diseñar para la eternidad

Si cada interfaz a userspace se mantiene para siempre, el corolario es brutal: hay que acertar a la primera, porque un error se arrastra durante décadas. De ahí el escrutinio feroz que recibe cada syscall nueva, cada ioctl, cada fichero de /sys —cada uno es ABI naciente— y de ahí un patrón de diseño que has de reconocer: la estructura extensible con campo de tamaño.

/* clone3, openat2, statx, sched_setattr: todas comparten esta idea.
 * userspace pasa un puntero a la struct y su tamano; el kernel se adapta. */
struct sched_attr {
	__u32 size;           /* el tamano que conoce quien llama */
	__u32 sched_policy;
	__u64 sched_flags;
	/* ... campos que fueron creciendo version a version ... */
};

long sched_setattr(pid_t pid, struct sched_attr *attr, unsigned int flags);

El ayudante interno copy_struct_from_user implementa la magia de la compatibilidad en ambos sentidos, resolviendo las cuatro combinaciones de binario y kernel sin ambigüedad:

                       kernel viejo (struct pequena)   kernel nuevo (struct grande)
binario viejo          coinciden: caso trivial          kernel rellena con ceros lo que falta
binario nuevo          exige ceros en el exceso o falla  coinciden: caso trivial

Si un binario viejo pasa una struct pequeña a un kernel nuevo, el kernel rellena con ceros los campos que aquel no conocía. Si un binario nuevo pasa una struct grande a un kernel viejo, el kernel exige que los bytes que no entiende sean todos cero, y si no lo son, falla limpio con -E2BIG en vez de malinterpretarlos. Añade a eso los argumentos flags reservados —que deben valer cero hasta que se les dé significado— y tienes interfaces que crecen sin romperse. La lección de diseño es que la extensibilidad se planifica el día cero, no se improvisa después.

El reverso de la moneda son las cicatrices: cuando un programa acaba dependiendo de un comportamiento accidental, el kernel lo conserva aunque fuera un error, y a veces lo detecta y lo emula. El caso arquetípico es el largo suplicio del time_t de 32 bits y el año 2038: no se pudo “arreglar” cambiando el tipo, porque miles de binarios y de estructuras en disco lo daban por sentado; hubo que introducir syscalls nuevas con sufijo _time64 conviviendo con las viejas para siempre. Congelar la frontera tiene ese precio: los errores de diseño no se corrigen, se acompañan de por vida.

Una ley de la ingeniería disfrazada de decisión moral

Detente en lo que esta regla dice de verdad, porque trasciende el kernel y toca la naturaleza misma del software que dura. Hay un teorema empírico, la ley de Hyrum, que enuncia: con suficientes usuarios de una interfaz, no importa lo que prometas en el contrato, absolutamente todos los comportamientos observables de tu sistema acabarán siendo aquello de lo que alguien depende. El kernel de Linux es la demostración más extrema de esa ley —tiene miles de millones de usuarios— y ante ella tenía dos caminos. Podía hacer lo que hace casi toda la industria: declarar “eso no estaba soportado, actualiza tu programa”, y trasladar al usuario el coste de la limpieza interna. O podía hacer lo que hizo Torvalds: aceptar que el usuario es soberano y que la carga de no romperle nada recae entera sobre el kernel, para siempre, sin excepciones ni por elegancia ni por corrección teórica. Eligió lo segundo, y esa elección parece técnica pero es moral. Es la negativa a que las personas paguen con su trabajo el deseo de los desarrolladores de tener el código bonito. El software libre le dio al mundo el código; esta regla le dio algo que el código por sí solo no garantiza: la permanencia, la promesa de que lo que construyas sobre Linux seguirá en pie cuando quienes lo escribieron ya no estén. Y la paradoja final, la que cierra tu modelo mental de todo el track, es que esa congelación de la frontera no encadena al kernel: lo libera. Precisamente porque el contrato externo es inviolable, el interior puede ser un solar en obras perpetuas donde todo se derriba y se reconstruye mejor. Estabilidad afuera y revolución adentro no son fuerzas opuestas: son la misma decisión vista desde sus dos caras.

⚔️ Interroga la frontera
  1. Lee la definición de openat2 o clone3 y localiza el campo de tamaño: explica qué ocurre cuando un binario viejo la invoca en un kernel nuevo, y al revés.
  2. Encuentra un mensaje de Torvalds revirtiendo un cambio por romper userspace y resume su argumento en una frase.
  3. Distingue, con un ejemplo de cada uno, un fichero de /sys que sea ABI estable de uno de debugfs que no lo sea, y justifica la diferencia.
  4. Explica por qué un módulo fuera del árbol no recibe ninguna garantía de estabilidad de API interna, y cómo se relaciona eso con el “upstream first” de la lección 1.
  5. Enuncia la ley de Hyrum con tus palabras y argumenta por qué el kernel es su caso límite.