Del comando al kernel: capas, syscalls y strace
Qué pasa entre que escribís ls y aparece la lista. User space, kernel space y las syscalls que cruzan la frontera, vistas con strace en vez de imaginadas.
Antes de empezar necesitás
- Una terminal Linux con strace, o el entorno con Podman de este lab
- Haber hecho el lab de permisos, usuarios y procesos
Al terminar vas a poder
- Ubicar las capas de un sistema Linux: programas, glibc, syscalls, kernel, drivers y hardware
- Explicar por qué un segfault mata un proceso y un bug en el kernel tira el sistema
- Leer una traza de strace y reconocer openat, read, write, clone, execve y wait4
- Ver el patrón fork + exec con el que la shell lanza cada comando
Para ver y leer
- NO SABES COMO FUNCIONA TU COMPUTADORA (y es un PROBLEMA)VT Security · YouTubeCapas, procesos, scheduler y syscalls: el mismo recorrido, explicado en video.
- Linux-Study-Notes-VT: notas de estudio para la LFCArepoLas notas originales en inglés de las que sale este lab (CC BY 4.0).
Entorno listo con Podman
Debian con strace para trazar ls y la shell sin tocar tu sistema. lab-check revisa tus trazas, el resumen de syscalls y el segfault que no tira nada.
# Una vez: cloná el repo y construí la imagen
git clone https://github.com/ValentinTorassa/Open-Security-Labs.git
cd Open-Security-Labs
podman build -t osl/del-comando-al-kernel -f entornos/linux-real/del-comando-al-kernel/Containerfile entornos
# Abrí el entorno (se borra al salir)
podman run --rm -it --name osl-del-comando-al-kernel --hostname labs osl/del-comando-al-kernel
# Al terminar, adentro del contenedor
lab-checklab-checkrevisa lo que hiciste y guarda el resultado en/home/vt/evidencia. Desde otra terminal, en la carpeta del repo,npm run lab:check -- linux-real/del-comando-al-kernelcorre el mismo chequeo y copia tu evidencia aevidencia/del-comando-al-kernel/.- ¿Usás Docker? Cambiá podman por docker en los comandos y funciona igual.
- Qué trae, qué revisa y sus límites: entornos/linux-real/del-comando-al-kernel/README.md.
Escribís ls y aparece una lista. En el medio, ls nunca tocó el disco: le pidió al kernel que abra un directorio, que le pase las entradas y que escriba el resultado en tu terminal. Este lab es sobre ese pedido, que tiene nombre (syscall) y se puede ver entero con strace.
Las capas
graph TD A["Programas<br/>bash · ls · tu código"] B["Biblioteca de C (glibc)<br/>envuelve las syscalls en funciones"] C["Interfaz de syscalls<br/>openat · read · write · clone · execve · mmap"] D["Kernel<br/>procesos · memoria · VFS · red"] E["Drivers<br/>hablan con cada dispositivo"] F["Hardware<br/>CPU · RAM · disco · placa de red"] A --> B --> C --> D --> E --> F
El código de user space corre en el ring 3 de la CPU. Las instrucciones que tocan hardware necesitan ring 0, donde corre el kernel. Si un programa intenta leer memoria que no es suya o hablarle directo a un dispositivo, la CPU genera una falla antes de que pase nada y el kernel mata al proceso. No es una política escrita en un archivo: lo hace el silicio.
La frontera: user space y kernel space
Cada proceso tiene su propio espacio de direcciones virtual. Las tablas de páginas que arma el kernel traducen esas direcciones a memoria física, y un proceso no tiene ninguna entrada que apunte a las páginas de otro. No hay un camino que bloquear: el camino no existe.
Eso explica la diferencia entre dos fallas que suenan parecidas:
segfault en user space el proceso muere, el kernel limpia y el sistema sigue
bug en el kernel no hay nadie más arriba que lo ataje: kernel panic
Probalo con un proceso que se manda a sí mismo SIGSEGV:
{ bash -c 'kill -SEGV $$'; echo "salida: $?"; }
# bash: line 1: 22 Segmentation fault (core dumped) bash -c 'kill -SEGV $$'
# salida: 139139 es 128 + 11, y 11 es el número de SIGSEGV. El hijo murió; tu shell siguió como si nada. Por eso un módulo del kernel de origen dudoso es un riesgo distinto a un programa con bugs: los módulos corren en ring 0, con todos los privilegios, y un error ahí no queda contenido.
Syscalls: la única puerta
Para cruzar de forma legítima, el programa pone el número de syscall en un registro (rax en x86-64), los argumentos en otros y ejecuta la instrucción syscall. La CPU pasa a ring 0, el kernel busca el handler en una tabla por ese número, hace el trabajo y devuelve el resultado. El programa nunca estuvo en ring 0: esperó.
| Syscall | Qué hace |
|---|---|
openat |
Abre un archivo y devuelve un file descriptor |
read / write |
Lee o escribe bytes sobre un file descriptor |
close |
Libera el file descriptor |
clone |
Crea un proceso hijo (lo que glibc usa para fork()) |
execve |
Reemplaza la imagen del proceso por otro programa |
mmap |
Mapea memoria; es lo que termina llamando malloc |
wait4 |
Espera a que un hijo termine |
exit_group |
Termina el proceso |
Casi nunca las llamás directo: glibc las envuelve. fopen() termina en openat, malloc() en mmap o brk, y los runtimes de Python, Go o Node se apoyan en la misma interfaz.
strace: lo que el programa hace de verdad
strace muestra cada syscall de un proceso con sus argumentos y su resultado. Es honesto: no muestra lo que el código dice que hace, muestra lo que le pide al kernel.
# Solo las aperturas de archivos
strace -e trace=openat ls /tmp
# Un resumen: cuántas veces se llamó cada syscall
strace -c ls /tmpLa primera línea de la traza de ls casi seguro es esta:
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
Es el linker dinámico buscando dónde están las bibliotecas compartidas, antes de que main() de ls empiece a correr. Después vienen libc.so.6, libselinux y compañía. Un “programa corriendo” es bastante más que el código que escribió su autor.
fork + exec: cómo la shell lanza un comando
Cada comando que escribís sigue el mismo patrón: la shell se clona, el hijo se reemplaza con el programa pedido y el padre espera.
strace -f -e trace=process bash -c 'ls >/dev/null; echo listo'execve("/usr/bin/bash", ["bash", "-c", "ls >/dev/null; echo listo"], ...) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, ...) = 21
[pid 20] wait4(-1, <unfinished ...>
[pid 21] execve("/usr/bin/ls", ["ls"], ...) = 0
[pid 21] exit_group(0) = ?
<... wait4 resumed>[{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 21
-f hace que strace siga a los hijos. El ; echo listo está a propósito: si le das a bash -c un solo comando, bash se ahorra el clone y hace execve directo, y la traza no muestra el patrón.
Root también es un número
El kernel nunca escuchó hablar de vt. Conoce el UID 1000, y todas sus decisiones de permisos se toman con números. Cada proceso lleva varios:
grep -E '^(Uid|Gid)' /proc/$$/status
# Uid: 1000 1000 1000 1000 real, efectivo, guardado y de filesystemEl que el kernel mira para decidir es el efectivo. Root es el UID 0 y nada más: el kernel tiene condiciones del tipo “si el UID efectivo es 0, saltá este chequeo”. sudo es un binario setuid: al ejecutarlo, su UID efectivo pasa a ser 0, revisa /etc/sudoers y deja un registro por comando. Trabajar siempre como root no deja ese registro y hace que cada typo corra con todos los privilegios.
Quiz
Comprobá lo que entendiste
0 / 3 correctas
1. Un programa hace un segfault. ¿Qué pasa con el resto del sistema?
2. ¿Cuál es el primer archivo que abre ls, antes de su main()?
3. Con strace -f, la shell lanza ls. ¿En qué orden aparecen las syscalls?
Lo que practicás en este lab
Llevátelo a tu repo si querés, pero no es obligatorio: es tu aprendizaje.
- Resumen de strace -c sobre ls con las syscalls que más se repiten
- El primer archivo que abre ls antes de llegar a main(), con la línea de strace que lo muestra
- Traza de bash -c lanzando un comando, con el clone, el execve y el wait4 marcados
- Writeup de tres líneas: dónde termina user space y por qué importa
Reto
Elegí un comando que uses todos los días (cat, grep, curl). Corré strace -c y strace -e trace=openat sobre él. Explicá en tres líneas qué archivos abrió que no esperabas y qué syscalls dominan.
Resolvelo y escribí dos líneas explicando qué pasó. Con eso lo fijás.