← Sistemas de Archivos y E/SBloque 8 · Permisos y protección · Tema 29 de 50

Tema 29

access y la carrera TOCTOU

Comprobar una ruta y usarla después no forma una sola operación. Aprenderemos qué pregunta access, cómo una entrada puede cambiar entre llamadas y por qué abrir, validar con fstat y conservar el descriptor evita resolver otra vez el nombre. Dos programas en C, una sustitución coordinada y dos simuladores harán visibles las garantías y los límites de cada estrategia.

1. Comprobar un nombre no reserva un objeto

En los temas 26 a 28 estudiamos permisos, credenciales y atributos. Ahora aparece otra dimensión: el tiempo. Aunque una consulta describa correctamente un archivo, la entrada que lleva hasta él puede cambiar antes de la operación siguiente.

TOCTOU significa time of check to time of use: una condición comprobada deja de estar garantizada cuando se usa el recurso. En archivos, un caso clásico consiste en consultar una ruta y resolverla de nuevo más tarde.

secuencia conceptual · dos resoluciones independientes
Proceso P                         Proceso Q
access("entrada", R_OK) → sí
                                  reemplaza entrada por otro objeto
open("entrada", O_RDONLY) → ?

La cadena de texto no cambió. El objeto al que conduce puede haber cambiado.

La sustitución no tiene que ser maliciosa: otro componente puede publicar una versión nueva mediante rename. El problema aparece cuando el programa da por conservada una relación que nunca aseguró.

2. Qué pregunta access

interfaz · consulta, no apertura
#include <unistd.h>
int access(const char *ruta, int modo);

F_OK             comprobar existencia accesible por esa ruta
R_OK             comprobar lectura
W_OK             comprobar escritura
X_OK             comprobar ejecución o búsqueda
R_OK | W_OK      petición conjunta

Retorno: 0 si la consulta se satisface; -1 y errno en caso de error.

F_OK vale cero: no es un bit para añadir a una petición. Un fallo no significa siempre «el archivo no existe»: puede faltar búsqueda en un directorio, haber un enlace colgante o existir otra causa. access sigue el enlace simbólico final.

En Linux, access realiza su comprobación con UID/GID reales; para capacidades hay reglas particulares según el UID real. No es una predicción general de lo que hará open, que normalmente opera con las credenciales de acceso al sistema de archivos del proceso. Véase access(2).

Una consulta puede servir para presentar información al usuario. Para realizar una operación, se intenta esa operación y se interpreta su retorno. El resultado previo no constituye una autorización que el kernel conserve.

3. Identidad real, efectiva y consultas relativas

En un proceso ordinario suelen coincidir los identificadores reales y efectivos. Eso oculta la diferencia entre preguntas. En un ejecutable con cambio de identidad, «¿podría el usuario real?» y «¿puede este proceso?» pueden producir respuestas distintas aun sin carrera.

InterfazReferencia y alcance
accessConsulta por ruta con las reglas de identidad real de esa interfaz.
faccessatConsulta relativa a un directorio; AT_EACCESS pide identidades efectivas en las interfaces que lo implementan.
faccessat2 en LinuxInterfaz del kernel con flags; la compatibilidad depende de kernel y biblioteca.
open / openatIntenta abrir y devuelve un descriptor o un error.

Seleccionar identidades efectivas en una consulta no une esa consulta con un open posterior. Tampoco se debe emular la política completa leyendo solo st_mode: las ACL y otras restricciones estudiadas siguen importando.

4. La ventana de carrera

La secuencia vulnerable mantiene una condición en memoria, pero vuelve a consultar un nombre compartido. Cualquier trabajo intermedio agranda la ventana; quitarlo o usar una máquina rápida no transforma dos llamadas en una transacción.

antipatrón · no usar access como autorización de la apertura
if (access(ruta, R_OK) == 0) {
    /* La ruta y los permisos pueden cambiar aquí. */
    int fd = open(ruta, O_RDONLY);
    /* Incluso si funciona, puede ser otro objeto. */
}

El problema tampoco se resuelve repitiendo access justo antes de abrir, usando una ruta absoluta o comparando cadenas. Un mutex protege solamente a participantes que lo respetan; no bloquea las modificaciones de cualquier otro proceso.

Hay dos resultados distintos que diagnosticar: la segunda operación puede fallar, o puede tener éxito sobre un objeto diferente. El segundo es especialmente fácil de pasar por alto.

5. Abrir y conservar la referencia

Una apertura satisfactoria devuelve un descriptor relacionado con una descripción de archivo abierta. read, fstat y las operaciones apropiadas por descriptor siguen esa referencia; no vuelven a buscar el nombre original para localizar el archivo.

patrón · adquirir, validar y usar la misma referencia
fd = open(ruta, opciones_de_apertura);
if (fd == -1) tratar_error();
if (fstat(fd, &st) == -1) tratar_error();
if (!cumple_politica(st)) rechazar_y_cerrar();
/* Leer o modificar a través de fd, sin reabrir ruta. */

Si después se reemplaza el nombre, el descriptor sigue refiriendo al objeto abierto. Esa garantía no afirma que se haya elegido el objeto correcto inicialmente. La política de resolución y la validación siguen siendo necesarias. Véase open(2).

6. Qué demuestra fstat y qué puede cambiar

fstat consulta el objeto referido por el descriptor. Permite comprobar tipo, propietario, grupo, modo y otros campos sin volver a resolver la ruta. Se utiliza S_ISREG para distinguir un regular de un directorio, FIFO u otro tipo.

La pareja st_dev y st_ino permite comparar identidad en las observaciones del ensayo. El número de inodo aislado no es único entre sistemas de archivos; además, los inodos pueden reutilizarse a lo largo del tiempo. Los campos de stat tampoco forman necesariamente una fotografía transaccional frente a cambios concurrentes. Véase stat(2).

El descriptor estabiliza la referencia, no el contenido ni todos los metadatos. Otro proceso con acceso al mismo objeto puede escribir o cambiar atributos. Si necesitas una versión coherente, debes definir además un protocolo de publicación, copia, bloqueo cooperativo u otro mecanismo adecuado.

fchmod(fd, modo) cambia el objeto abierto, mientras chmod(ruta, modo) resuelve otra vez el nombre. Esa diferencia continúa siendo relevante después de validar. Véase chmod(2).

7. Evitar efectos antes de validar

Si abres con O_TRUNC para decidir después si el archivo era aceptable, puedes haber destruido contenido antes del rechazo. Para una lectura, usa una apertura de lectura; para una modificación, diseña el orden según los efectos concretos que necesitas permitir.

O_NONBLOCK evita que la apertura de una FIFO de lectura quede esperando a un escritor en estos ejemplos. No convierte cualquier dispositivo o sistema de archivos en una operación instantánea. Abrir tipos especiales puede tener efectos antes de fstat, por lo que un lector general de rutas hostiles necesita controles adicionales.

Los programas del tema se ejecutan sin privilegios en un árbol de práctica propio. La validación que muestran es un contrato de ejemplo, no una biblioteca universal para abrir cualquier objeto del sistema.

8. O_NOFOLLOW y los componentes intermedios

La resolución recorre componentes. Una ruta absoluta fija el punto inicial, pero no impide que un directorio intermedio sea un enlace o que una entrada cambie. Canonizar con realpath y abrir el resultado son también operaciones separadas.

ruta · el enlace puede no ser el último componente
base/enlace/nota
     ^ componente intermedio

O_NOFOLLOW sobre la apertura de esta ruta no prohíbe por sí solo ese enlace.
Para el enlace final, open ordinario con O_NOFOLLOW rechaza seguirlo.

O_NOFOLLOW no prohíbe enlaces duros. Dos nombres duros de un regular pueden abrir el mismo objeto sin ninguna resolución de enlace simbólico. Véase path_resolution(7).

9. openat y una base de directorio

openat permite resolver una ruta relativa respecto de un descriptor de directorio. Esa base sigue identificando el directorio aunque se renombre. Una ruta absoluta ignora la base en el openat ordinario, y .. puede salir de ella.

contrato reducido · un único nombre bajo una base confiable
base = open(directorio_confiable, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
/* Antes: rechazar nombre vacío, ".", ".." y cualquier '/'. */
fd = openat(base, nombre_simple, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);

Limitar la entrada a un componente elimina los directorios intermedios de esa entrada. Debe justificarse por separado cómo se obtuvo la base. O_NOFOLLOW al abrir el directorio solo se refiere a su componente final, no a todo el camino que llevó hasta él.

10. openat2 y políticas de resolución en Linux

Para rutas relativas de varios componentes, openat2 ofrece restricciones aplicadas durante la resolución. RESOLVE_BENEATH impide escapar por encima de la base y rechaza rutas absolutas; RESOLVE_NO_SYMLINKS prohíbe enlaces simbólicos en los componentes.

esquema Linux · comprobar siempre el retorno
struct open_how how = {0};
how.flags = O_RDONLY | O_CLOEXEC;
how.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS;
long fd = syscall(SYS_openat2, base, ruta_relativa, &how, sizeof how);

RESOLVE_IN_ROOT trata la base como raíz para esa resolución; no es sinónimo de BENEATH. RESOLVE_NO_XDEV añade restricciones sobre cruces de montajes. Estas políticas no excluyen por sí solas enlaces duros ni validan propietario o contenido.

El soporte requiere kernel e interfaces adecuados. Si la política es necesaria y no puede aplicarse, no se sustituye silenciosamente por una apertura más permisiva. Véase openat2(2).

11. Crear sin una comprobación previa de ausencia

«Si no existe, créalo» también admite una carrera. Para exigir un nombre nuevo se combinan O_CREAT y O_EXCL en la misma apertura y se comprueba EEXIST. El argumento de modo se interpreta en octal en C y participa en las reglas de creación de los temas anteriores.

creación · decidir existencia dentro de la operación
int fd = openat(base, "nuevo", O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (fd == -1) {
    /* EEXIST no se resuelve truncando sin más el destino. */
}

Para temporales, mkstemp crea y abre un nombre a partir de una plantilla modificable, sin separar elección de nombre y creación. Se conserva el descriptor devuelto. Véase mkstemp(3).

Publicar mediante rename en el mismo sistema de archivos puede reemplazar una entrada de forma atómica para sus observadores. Eso no obliga a los lectores que ya abrieron el objeto anterior a cambiar de versión ni constituye por sí solo una garantía de persistencia ante apagados. Véase rename(2).

12. Programa en C: observar una sustitución coordinada

Guarda demo-toctou.c. El modo ruta consulta access y espera antes de abrir. El modo descriptor abre y valida antes de esperar. La línea LISTO permite al verificador reemplazar exactamente la entrada entre fases, sin depender de sleep ni de ganar una carrera probabilística.

demo-toctou.c · ejecutar solo con los archivos propios del laboratorio
#define _POSIX_C_SOURCE 200809L
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <inttypes.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

static int mostrar(int fd, const char *etapa)
{
    struct stat st;
    if (fstat(fd, &st) == -1) { perror("fstat"); return -1; }
    if (!S_ISREG(st.st_mode)) {
        fprintf(stderr, "Se esperaba un archivo regular\n"); return -1;
    }
    printf("%s dev=%ju ino=%ju\n", etapa,
           (uintmax_t)st.st_dev, (uintmax_t)st.st_ino);
    return 0;
}

int main(int argc, char **argv)
{
    if (argc != 3 || (strcmp(argv[1], "ruta") && strcmp(argv[1], "descriptor"))) {
        fprintf(stderr, "Uso: %s ruta|descriptor archivo\n", argv[0]);
        return 2;
    }
    int por_ruta = strcmp(argv[1], "ruta") == 0;
    int fd = -1, resultado = 1;
    if (por_ruta) {
        /* Patrón incorrecto como autorización de un open posterior. */
        if (access(argv[2], R_OK) == -1) { perror("access"); return 1; }
        puts("access: lectura admitida en este instante");
    } else {
        fd = open(argv[2], O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
        if (fd == -1) { perror("open inicial"); return 1; }
        if (mostrar(fd, "ABIERTO") == -1) goto fin;
    }
    puts("LISTO");
    if (fflush(stdout) == EOF) { perror("fflush"); goto fin; }
    if (getchar() != '\n') {
        fprintf(stderr, "Se esperaba una línea vacía para continuar\n"); goto fin;
    }
    if (por_ruta) {
        fd = open(argv[2], O_RDONLY | O_CLOEXEC | O_NONBLOCK);
        if (fd == -1) { perror("open posterior"); goto fin; }
    }
    if (mostrar(fd, "USADO") == -1) goto fin;
    unsigned char datos[16];
    ssize_t n;
    do { n = read(fd, datos, sizeof datos); } while (n == -1 && errno == EINTR);
    if (n == -1) { perror("read"); goto fin; }
    printf("DATOS_HEX=");
    for (ssize_t i = 0; i < n; ++i) printf("%02x", (unsigned int)datos[i]);
    putchar('\n');
    resultado = 0;
fin:
    if (fd != -1 && close(fd) == -1) { perror("close"); resultado = 1; }
    return resultado;
}

El programa imprime una muestra hexadecimal de hasta 16 bytes, no el archivo completo. read puede devolver menos bytes de los solicitados; el retorno es la longitud válida, incluido cero al llegar al final. Véase read(2).

El modo ruta es una demostración del error, no una recomendación. La sustitución del ensayo utiliza dos regulares legibles de la misma cuenta, por lo que un resultado de B prueba cambio de objeto, sin afirmar una escalada de privilegios.

13. Programa en C: lectura relativa con contrato explícito

Guarda leer-relativo.c. Acepta una base de confianza y un nombre simple. Rechaza el enlace simbólico final, consulta fstat y exige un regular propio sin bits de escritura para grupo u otros. La lectura usa el mismo descriptor que se validó.

leer-relativo.c · una muestra de hasta 32 bytes
#define _POSIX_C_SOURCE 200809L
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
    if (argc != 3 || !argv[2][0] || strchr(argv[2], '/') ||
        !strcmp(argv[2], ".") || !strcmp(argv[2], "..")) {
        fprintf(stderr, "Uso: %s directorio-confiable nombre-simple\n", argv[0]);
        return 2;
    }
    int base = open(argv[1], O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (base == -1) { perror("open directorio"); return 1; }
    int fd = openat(base, argv[2], O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
    int resultado = 1;
    if (fd == -1) { perror("openat"); goto fin; }
    struct stat st;
    if (fstat(fd, &st) == -1) { perror("fstat"); goto fin; }
    if (!S_ISREG(st.st_mode) || st.st_uid != geteuid() || (st.st_mode & 0022)) {
        fprintf(stderr, "Rechazado: se exige regular propio sin escritura de grupo/otros\n");
        goto fin;
    }
    unsigned char datos[32];
    ssize_t n;
    do { n = read(fd, datos, sizeof datos); } while (n == -1 && errno == EINTR);
    if (n == -1) { perror("read"); goto fin; }
    printf("MUESTRA_HEX=");
    for (ssize_t i = 0; i < n; ++i) printf("%02x", (unsigned int)datos[i]);
    putchar('\n');
    resultado = 0;
fin:
    if (fd != -1 && close(fd) == -1) { perror("close archivo"); resultado = 1; }
    if (close(base) == -1) { perror("close directorio"); resultado = 1; }
    return resultado;
}

Esta política es deliberadamente pequeña. No comprueba exclusividad frente a otros procesos del mismo UID, ni garantiza una versión inmutable de los bytes. Tampoco certifica que el objeto se originó dentro de la base: podría tener otros enlaces duros. La ruta para obtener la base y sus administradores deben ser de confianza.

14. Laboratorio y verificación reproducible

Usa Bash en Linux con una cuenta ordinaria, compilador C y Python 3. No hacen falta sudo, cuentas adicionales ni archivos del sistema. Crea un directorio privado nuevo y guarda allí los dos programas anteriores.

terminal · preparación y compilación
$ LAB29=$(mktemp -d "$HOME/tema29.XXXXXX")
$ cd -- "$LAB29"
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 demo-toctou.c -o demo-toctou
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 leer-relativo.c -o leer-relativo

Guarda el siguiente verificador como verificar-tema29.py. Cada ejecución crea casos nuevos y conserva el árbol. La sustitución se hace después de recibir LISTO y antes de enviar la línea que permite continuar.

verificar-tema29.py · intercambio determinista de la entrada
import os
from pathlib import Path
import subprocess
import tempfile

base = Path(tempfile.mkdtemp(prefix="casos29-", dir=".")).resolve()
demo = Path("demo-toctou").resolve()
lector = Path("leer-relativo").resolve()

def carrera(modo):
    d = base / modo
    d.mkdir(mode=0o700)
    entrada, reemplazo = d / "entrada", d / "reemplazo"
    entrada.write_bytes(b"A\n")
    reemplazo.write_bytes(b"B\n")
    original = entrada.stat()
    p = subprocess.Popen([str(demo), modo, str(entrada)],
                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE, text=True)
    try:
        lineas = []
        for linea in p.stdout:
            lineas.append(linea)
            if linea.strip() == "LISTO":
                break
        else:
            raise AssertionError("El programa no alcanzó LISTO")
        # El proceso está detenido antes del open o después, según el modo.
        os.replace(reemplazo, entrada)
        salida, error = p.communicate("\n", timeout=5)
        texto = "".join(lineas) + salida
        assert p.returncode == 0, error
        esperado = b"B\n" if modo == "ruta" else b"A\n"
        assert "DATOS_HEX=" + esperado.hex() in texto, texto
        assert entrada.read_bytes() == b"B\n"
        assert entrada.stat().st_ino != original.st_ino
        print(modo + ": " + texto.replace("\n", " | "))
    finally:
        if p.poll() is None:
            p.kill()
        p.communicate()

for modo in ("ruta", "descriptor"):
    carrera(modo)

d = base / "lectura"
d.mkdir(mode=0o700)
(d / "nota").write_bytes(b"curso29\n")
os.chmod(d / "nota", 0o600)
(d / "alias").symlink_to("nota")
(d / "carpeta").mkdir()
os.mkfifo(d / "fifo", 0o600)
(d / "amplio").write_bytes(b"dato")
os.chmod(d / "amplio", 0o622)

def ejecutar(nombre):
    return subprocess.run([str(lector), str(d), nombre],
                          capture_output=True, text=True, timeout=5)

p = ejecutar("nota")
assert p.returncode == 0 and "MUESTRA_HEX=" + b"curso29\n".hex() in p.stdout
for nombre in ("alias", "carpeta", "fifo", "amplio", "ausente"):
    assert ejecutar(nombre).returncode == 1, nombre
for nombre in ("", ".", "..", "../nota", "/etc/passwd", "carpeta/nota"):
    assert ejecutar(nombre).returncode == 2, nombre
print("Correctos: sustitución coordinada, descriptor estable y contrato de lectura")
print("Laboratorio conservado en", base)
terminal · comprobar el resultado
$ python3 verificar-tema29.py
...
Correctos: sustitución coordinada, descriptor estable y contrato de lectura
Laboratorio conservado en ...

En ruta se lee B; en descriptor se lee A, aunque entrada ya contenga B. El verificador comprueba además nombres inválidos, enlace simbólico, directorio, FIFO, permisos demasiado amplios y ausencia del archivo. La FIFO no debe dejar el ensayo bloqueado.

15. Simuladores de secuencia y resolución

Avanzar por la ventana de carrera

El modelo supone dos regulares propios e inmutables durante el ensayo: A inicial y B sustituto. Todas las aperturas y permisos se satisfacen. Observa qué objeto refiere el nombre y cuál conserva el descriptor.

Nombre compartido y referencia abierta

estado actual del modelo
FaseOperaciónNombreDescriptor

Comparar políticas de resolución

La base ya fue adquirida correctamente. El panel evalúa solo si la política descrita rechaza el recorrido; no simula los permisos del kernel, montajes, capacidades o errores de E/S. El caso ../fuera sale de la base.

Qué restricción alcanza cada componente

alcance y límite de la política

16. Errores y falsas soluciones

SituaciónDecisión adecuada
access falló.Registrar errno de esa llamada; no reducir todos los fallos a ENOENT.
open falló tras una consulta exitosa.Tratar el fallo de open; no asumir que su éxito estaba reservado.
fstat no cumple el contrato.Cerrar y rechazar antes de usar datos o modificar contenido.
openat2 no puede aplicar la política.Fallar de forma explícita o usar otro diseño que ofrezca la garantía requerida.
La ruta sigue dentro del árbol, pero hay un enlace duro.Evaluar procedencia y política del objeto, además de la resolución.
El descriptor es estable, pero cambian los bytes.Definir cómo se publican y coordinan las versiones del contenido.

Revisar permisos antes de cada read no convierte un descriptor en una instantánea. Tampoco cerrar y reabrir por el nombre después de validar conserva la garantía que se había obtenido.

17. Resumen

18. Preguntas de repaso y práctica

1. ¿Qué significa TOCTOU?

Una condición comprobada puede dejar de ser válida entre la consulta y el uso del recurso.

2. ¿access devuelve un descriptor?

No. Devuelve cero o un error para su consulta; no mantiene abierto el archivo.

3. ¿F_OK se combina para añadir una prueba de existencia?

F_OK vale cero y se usa como petición de existencia. Las peticiones de lectura o escritura ya necesitan resolver el objeto.

4. ¿Un fallo de access prueba ausencia?

No. Puede faltar búsqueda en un directorio o haber otras causas; hay que interpretar errno.

5. ¿UID real y efectivo siempre coinciden?

No. Un programa con cambio de identidad hace visible por qué las consultas y operaciones pueden responder a preguntas distintas.

6. ¿AT_EACCESS elimina la carrera?

No. Cambia la selección de identidades de la consulta, no une consulta y uso.

7. ¿Repetir access junto a open los hace atómicos?

No. Siguen siendo dos operaciones con una ventana entre ellas.

8. ¿Una ruta absoluta garantiza identidad?

No. Fija el inicio del recorrido, pero las entradas y componentes pueden cambiar.

9. ¿Qué objeto consulta fstat?

El referido por el descriptor, sin resolver de nuevo el nombre original.

10. ¿El descriptor cambia de objeto si se sustituye su nombre?

No. Conserva la referencia al objeto abierto.

11. ¿El descriptor impide escrituras de otros procesos?

No. Estabiliza la referencia, no bloquea el contenido.

12. ¿O_TRUNC es una buena forma de abrir antes de validar?

Puede destruir contenido durante la apertura. Deben evitarse efectos que la validación aún no autorizó.

13. ¿O_NOFOLLOW prohíbe enlaces intermedios?

Por sí solo en una apertura ordinaria se refiere al componente final.

14. ¿openat bloquea ..?

No. El openat ordinario no es una política de confinamiento.

15. ¿Por qué el lector exige un nombre simple?

Para que la entrada no introduzca componentes intermedios, rutas absolutas ni recorridos con .. .

16. ¿RESOLVE_BENEATH rechaza enlaces duros?

No. Controla la resolución bajo una base; un enlace duro requiere otra consideración de política.

17. ¿Qué combina O_CREAT | O_EXCL?

La creación y la exigencia de que el nombre no exista, dentro de esa apertura.

18. ¿Por qué el laboratorio espera LISTO?

Para colocar la sustitución entre fases concretas, sin depender de tiempos ni probabilidades.

19. ¿Leer B en modo ruta demuestra elevación?

No. Ambos archivos son propios y legibles. Demuestra que el objeto usado puede diferir del consultado.

20. ¿Por qué no reabrir por ruta tras fstat?

La reapertura resolvería otra vez el nombre y perdería la referencia validada.

Para practicar

  1. Dibuja una sustitución que haga fallar open después de un access exitoso.
  2. Ejecuta el laboratorio y compara st_dev/st_ino y las muestras de A y B.
  3. Haz el intercambio después de abrir en ambas estrategias y explica el resultado.
  4. Agrega al verificador un archivo vacío y explica el retorno cero de read.
  5. Compara O_NOFOLLOW sobre enlace final e intermedio en un árbol propio.
  6. Explica por qué una base confiable y un nombre simple son requisitos distintos.
  7. Diseña una política explícita para un archivo de configuración, indicando qué identidad y qué tipo exige.
  8. Describe una política de publicación que evite modificar archivos ya publicados.
  9. Separa qué problemas resuelven O_EXCL, O_NOFOLLOW y RESOLVE_BENEATH.
  10. Revisa un patrón stat → chmod por ruta y propón una operación por descriptor cuando sea aplicable.

En el tema 30 comenzaremos el bloque de montaje: cómo conectar sistemas de archivos al árbol y practicar con una imagen loop.