CAPÍTULO 17 · PARTE II

Archivos, metadatos, permisos y persistencia

Cómo un sistema operativo resuelve nombres en objetos, separa datos de metadatos, aplica permisos y convierte una escritura en estado durable; el capítulo acota qué puede concluirse sobre acceso, integridad y recuperación.

Nivel N2 · Estado published

Un archivo no es sólo sus bytes

Cuando una investigación dice que «el archivo era accesible», todavía faltan varias preguntas. ¿Qué nombre se resolvió, desde qué directorio y con qué identidad? ¿El proceso obtuvo un descriptor antes de que cambiaran los permisos? ¿Se observó el contenido, una entrada de directorio o sólo un metadato? ¿La escritura sobrevivió a un reinicio o sólo estuvo visible en la caché? Estas preguntas describen capas distintas del mismo sistema.

Este capítulo construye un modelo operativo para responderlas. El alcance principal es el modelo de archivos de Unix/POSIX y su realización habitual en Linux. POSIX define interfaces y contratos; Linux añade el Virtual File System (VFS) y detalles de resolución, permisos y almacenamiento que dependen de la versión y del sistema de archivos. No se debe trasladar una conclusión concreta a Windows, a un sistema distribuido o a otro filesystem sin revisar sus propias garantías.

La tesis es sencilla, pero sus consecuencias son importantes: un pathname es una instrucción de búsqueda, un metadato describe un objeto, un permiso condiciona una operación y la persistencia es una propiedad de supervivencia frente a un evento definido. Ninguno sustituye a los otros.

¿Qué objeto se obtiene al resolver una ruta?

La resolución pasa de pathname a entrada de directorio, objeto e inodo; open crea una open file description y devuelve un descriptor del proceso.
Resolver un nombre encuentra un objeto; abrirlo crea una referencia que puede sobrevivir a cambios del nombre.

`stat(path)` y `fstat(fd)` pueden describir objetos distintos después de un reemplazo.

Un pathname es una secuencia de componentes separados por /. var/app/config no es el archivo; es una forma de pedir al sistema que empiece en un directorio y resuelva sucesivamente var, app y config. Para una ruta absoluta el punto de partida suele ser la raíz del proceso; para una relativa interviene el directorio de trabajo o un descriptor de directorio usado como referencia. path_resolution(7) describe también cómo entran en juego .., enlaces simbólicos, montajes y, en Linux moderno, restricciones como las de openat2.

Cada componente se busca en un directorio padre. El resultado de esa búsqueda es una directory entry: la asociación entre un nombre y el objeto al que apunta. El nombre es local a ese directorio; no es una identidad global del objeto. Dos nombres pueden apuntar al mismo objeto regular mediante hard links. Un symbolic link, en cambio, es otro objeto cuyo contenido es una ruta que se vuelve a resolver. Esta diferencia importa al borrar, cambiar permisos y revisar una carrera de tiempo.

En Linux el VFS mantiene en memoria dentries como ayuda para resolver nombres y trabaja con un inode (inodo) para representar el objeto: archivo regular, directorio, FIFO, socket o dispositivo, entre otros. El inodo contiene metadatos que la implementación expone mediante interfaces como stat: tipo, propietario, grupo, modo, tamaño, número de enlaces y marcas temporales, con variaciones de precisión y semántica. Los bytes de un archivo regular son sólo una parte de esta representación.

Un profesional debe separar estas observaciones:

La salida de ls -l es una vista cómoda, no el modelo completo. Un análisis de incidente puede necesitar stat, lstat, el descriptor que realmente abrió el proceso, el punto de montaje, ACL, capacidades y registros de la operación. Incluso un número de inodo sólo tiene significado dentro de su sistema de archivos y ventana de observación.

Nombre, descriptor y objeto abierto

La llamada open() resuelve una ruta y, si las comprobaciones tienen éxito, devuelve un file descriptor (descriptor de archivo). Ese número es un índice de la tabla del proceso. Apunta a una open file description, que conserva el objeto abierto y estado de operación como el offset. Las lecturas y escrituras posteriores usan esa referencia; no vuelven a preguntar qué objeto tiene ahora el pathname original.

Esta separación evita un error habitual. Un operador puede abrir report, otro proceso reemplazar la entrada por otro inodo y el primer descriptor seguir apuntando al objeto antiguo. La salida de stat("report") y la de fstat(fd) pueden referirse a objetos diferentes sin que ninguna llamada mienta. fstat inspecciona la referencia abierta; stat resuelve de nuevo el nombre; lstat inspecciona el enlace simbólico en lugar de seguirlo. La elección de interfaz debe corresponder a la pregunta.

También explica por qué comprobar primero y usar después puede ser defectuoso. El patrón «stat(path); luego open(path)» tiene una ventana en la que el nombre puede cambiar. Si la decisión de seguridad depende del objeto que se comprobó, la comprobación y el uso deben vincularse mediante un descriptor, openat/openat2, flags adecuados y una política de la aplicación. No basta con añadir una comprobación textual de prefijo: un enlace, un montaje o un cambio concurrente puede alterar la resolución.

¿Qué describen realmente los metadatos?

Los metadatos son datos sobre un objeto y su relación con el sistema de archivos. Entre los campos comunes están el tipo, propietario, grupo, modo, tamaño, número de enlaces, dispositivo y tiempos de cambio, modificación o acceso. La disponibilidad y precisión de cada campo dependen de la interfaz y del filesystem. Un mtime registra una actualización de contenido o metadatos según reglas de la implementación; no registra intención humana ni constituye un sello de autenticidad.

En este nivel conviene no confundir tres representaciones. Una entrada de directorio expresa la asociación de un nombre con un objeto dentro del namespace del filesystem. El VFS de Linux puede representar y cachear esa relación mediante un objeto dentry en memoria. El registro persistente que materializa la asociación depende del filesystem concreto y no tiene por qué ser idéntico a la estructura VFS. El capítulo usa «entrada de directorio» como abstracción nombre–objeto; cuando una conclusión dependa de caché, recuperación o formato físico deberá nombrar la capa observada.

El tipo también debe comprobarse. Una ruta con extensión .log puede resolver a un directorio, un enlace o un archivo regular. La extensión pertenece al nombre, no al contrato del objeto. Un proceso que decide el parser o la acción únicamente por esa extensión confunde una convención de presentación con una propiedad técnica.

Los enlaces muestran otra distinción. Un hard link es otra entrada que referencia el mismo inodo; eliminar un nombre no elimina el objeto mientras quede alguna referencia de enlace o descriptor. Un symbolic link tiene un inodo propio y apunta a un pathname; puede romperse o llevar fuera del directorio que el desarrollador consideraba seguro. stat normalmente sigue el enlace y lstat permite observarlo como enlace. Una política de «sólo archivos dentro de /srv/app» debe especificar si restringe nombres, objetos, montajes y enlaces, y con qué operación se verifica.

El VFS proporciona una interfaz común, pero no borra diferencias. Un filesystem puede tener semánticas particulares para tiempos, cuotas, snapshots, atributos extendidos o sincronización. Un almacén remoto puede confirmar una escritura con condiciones diferentes a un disco local. Por eso un claim profesional debe decir «en la versión y filesystem evaluados» cuando la diferencia pueda cambiar el resultado.

¿Cómo se decide si una operación está permitida?

Dos paneles comparan r, w y x en un archivo regular y en un directorio; el recorrido de directorios y políticas adicionales condicionan el resultado.
Las mismas letras expresan operaciones distintas sobre archivos y directorios.

Los bits de modo no sustituyen el recorrido, la identidad efectiva ni las políticas adicionales.

En el modelo tradicional de Unix, los bits de modo separan tres clases principales: propietario, grupo y otros. Para cada clase aparecen r (read), w (write) y x (execute). En un archivo regular, r suele permitir leer bytes, w escribirlos y x solicitar su ejecución, sujeto a formato, intérprete y otras políticas. En un directorio la semántica cambia: r permite enumerar nombres, w modificar entradas y x atravesar o resolver componentes. Tener r sobre un archivo no ayuda si el proceso no puede atravesar todos los directorios padres.

El bit x de un directorio no significa «ejecutar la carpeta». Significa que el proceso puede usarla como parte de una resolución o acceder a una entrada cuyo nombre conoce, según las demás condiciones. Así, un directorio puede impedir listar nombres y todavía permitir acceso a un nombre conocido. Esta separación aparece en revisiones de backups, exposición de sockets y rutas de configuración.

El kernel calcula la decisión con una identidad efectiva y el contexto de la operación. El propietario, grupo y bits son sólo una base. POSIX ACL puede añadir entradas más específicas; Linux capabilities pueden modificar ciertas comprobaciones; controles Mandatory Access Control, como una política de seguridad adicional, pueden denegar lo que los bits permitirían; y los montajes, namespaces o id mappings pueden cambiar qué árbol ve el proceso. La presencia de 0644 no autoriza a afirmar que «cualquiera puede leer» sin declarar esas capas.

La umask interviene al crear objetos: filtra permisos solicitados por el proceso, pero no es un guardián universal de todas las operaciones posteriores. Tampoco corrige una ruta que apunta al objeto equivocado. El propietario puede cambiar permisos, un administrador puede operar con privilegios y una aplicación puede compartir bytes por otra interfaz. Las autorizaciones deben evaluarse en el flujo real: principal, objeto, operación, momento, política y resultado.

Ejemplo de resolución y permisos

Supongamos un servicio que intenta abrir /srv/app/config/settings.json como un usuario de servicio. La investigación no debería empezar por el 0640 de settings.json. Debe reconstruir:

  1. qué raíz, namespace y directorio inicial usó el proceso;
  2. qué inodo resolvió para srv, app, config y el objeto final;
  3. si cada directorio permitió el recorrido (x) y si el objeto permitió la operación (r o w);
  4. si un enlace, montaje, ACL, capability o política MAC cambió la decisión;
  5. si el descriptor abierto quedó asociado al mismo inodo durante toda la operación.

Una observación «permission denied» localiza el resultado, pero no identifica sin más qué componente negó. Una observación positiva tampoco prueba que otra identidad, otro namespace o una llamada distinta obtendría el mismo resultado. El control positivo puede ser un archivo sintético con la política prevista; el negativo, un directorio padre sin x o un objeto ajeno. Ambos deben estar autorizados y no deben modificar datos de producción.

¿Qué significa persistencia en un sistema de archivos?

La actualización pasa por temporal validado, fsync exitoso del archivo, rename, fsync exitoso del directorio y verificación después de un fallo declarado.
Visibilidad del nombre y durabilidad frente a una caída son propiedades diferentes.

La secuencia sólo sostiene el alcance documentado por el sistema de archivos, el dispositivo y el modelo de fallo evaluados.

En este capítulo persistencia significa que un estado relevante sigue disponible y con la semántica esperada después de un evento declarado: cierre normal, reinicio, pérdida de alimentación o fallo del proceso. Es más preciso que decir «se escribió en disco». Un proceso puede devolver éxito después de copiar datos a una caché; otro lector puede verlos y, aun así, el contenido o la entrada de directorio no estar garantizados tras una caída.

Hay tres preguntas que suelen mezclarse:

El buffer de la biblioteca, la caché del kernel, el filesystem, la caché del dispositivo y el medio persistente pueden formar una cadena de estados. fflush mueve datos de la biblioteca de usuario; no equivale por sí solo a fsync. fsync(fd) solicita que los datos y metadatos modificados del archivo lleguen al almacenamiento permanente según las garantías de Linux y del dispositivo. La entrada de directorio que nombra ese archivo es otro metadato: para hacer durable el cambio de nombre puede ser necesario sincronizar también un descriptor del directorio. El alcance exacto depende de la implementación; el claim debe declararlo.

Reemplazo temporal y rename

Un patrón profesional para actualizar configuración es escribir una versión temporal en el mismo sistema de archivos, validar su contenido, sincronizar el descriptor del temporal, reemplazar el nombre visible con rename y sincronizar el directorio. Cada operación debe comprobar su resultado; continuar tras una escritura corta o tras el fallo de fsync() o rename() invalida la garantía pretendida. El orden busca evitar que un proceso encuentre un archivo parcialmente escrito y hace explícitas las fases de recuperación.

Una versión sintética, sólo para mostrar la secuencia y no para ejecutar contra datos reales, sería:

crear config.tmp en el directorio de destino
escribir bytes completos y validar formato
fsync(descriptor de config.tmp)
rename(config.tmp, config)
fsync(descriptor del directorio)

La secuencia sólo se considera completada si se verificaron escrituras y retornos, se cerraron los descriptores según el contrato y se comprobó la postcondición relevante. Una llamada que devuelve éxito aporta evidencia sobre esa interfaz y ese momento; no sustituye una prueba de recuperación cuando el claim exige supervivencia a una caída.

rename cambia la asociación de nombres y en condiciones previstas evita una ventana en la que config sea visible como una mezcla de dos contenidos. No convierte la actualización en transacción de la aplicación: la validación puede haber usado una versión distinta, otros procesos pueden conservar un descriptor antiguo y una caída puede dejar estados que el sistema concreto documenta de forma diferente. Si temporal y destino están en sistemas de archivos distintos, rename no es la misma operación y puede fallar.

Journaling tampoco significa que la aplicación recupere siempre el último estado semánticamente válido. Un journal puede ayudar a recuperar estructuras del filesystem; no sabe si dos archivos forman una transacción de negocio ni si un registro es aceptable para el servicio. La aplicación debe definir la unidad de actualización, el marcador de versión, la detección de estado incompleto y la estrategia de recuperación.

Metadatos, evidencia y límites de inferencia

En triage, es frecuente encontrar un archivo, una entrada de arranque o una modificación reciente y llamarlo «persistencia». Esa palabra describe una hipótesis sobre la supervivencia y el efecto del estado, no una intención observada. Una entrada en un directorio de inicio puede probar que existe una configuración; para afirmar ejecución en el próximo arranque hacen falta la semántica del componente que la consume, identidad de ejecución, condiciones de activación y una prueba controlada o evidencia operacional.

Los tiempos deben conservar procedencia, zona, precisión y reloj de la fuente. ctime en Linux no es «creation time»: suele registrar cambios del inodo, como modo, propietario o enlaces. mtime no prueba quién escribió; atime puede estar desactivado o actualizado con políticas del montaje. Un hash confirma que dos bytes producen el mismo digest bajo el algoritmo elegido, pero no prueba origen, momento ni ausencia de cambios antes de calcularlo. Un propietario es un metadato de identidad administrativa, no una atribución humana automática.

La adquisición también puede alterar el objeto: abrir, leer o montar un volumen puede actualizar tiempos, generar logs o cambiar el contexto. En forensics se trabaja sobre una pregunta concreta y se conserva una copia o imagen con procedencia, hash, cadena de custodia y documentación de las operaciones. «Recolectar todo» no sustituye a definir qué claim se intenta sostener.

Fallos frecuentes que degradan un control

Comprobar y usar una ruta separadamente. La ruta puede cambiar entre stat y open. El remedio no es confiar en el nombre, sino usar referencias abiertas, flags de no seguimiento o resolución confinada, y probar la carrera dentro de un entorno autorizado.

Tratar los bits como la política completa. ACL, capabilities, MAC, namespaces y privilegios pueden ampliar o restringir el resultado. La revisión debe registrar identidad efectiva y política aplicada.

Confundir rename con durabilidad. La visibilidad ordenada y la supervivencia a una caída son preguntas diferentes. Hay que pedir evidencia de sincronización, filesystem, dispositivo y recuperación.

Cambiar permisos del archivo final durante una actualización. Un temporal puede heredar permisos indeseados; un reemplazo puede cambiar propietario, ACL o etiquetas aunque el nombre sea el mismo. La política debe especificar metadatos esperados y verificarlos después del reemplazo.

Usar un nombre como frontera. Prefijos y extensiones no contienen enlaces, montajes ni cambios concurrentes. El descriptor y las primitivas de resolución deben corresponder al límite que se quiere imponer.

Inferir intención de un único artefacto. Un archivo reciente puede ser una actualización legítima, una copia restaurada o una modificación no autorizada. Correlacionar proceso, identidad, reloj, logs y estado previo permite discriminar hipótesis; no autoriza a elegir una historia por intuición.

Uso profesional: diseño, hardening y respuesta

En diseño de software, una actualización segura empieza por el claim: «el servicio sólo consume configuraciones validadas y, tras un reinicio definido, no observa un estado parcial». De ahí se derivan requisitos sobre ubicación, propietario, modo, ACL, tamaño, formato, versión, proceso escritor y protocolo de recuperación. La revisión debe incluir el caso en que el directorio no sea confiable, el disco se llene, el proceso muera entre pasos y dos instancias actualicen a la vez.

En hardening, no basta con convertir un archivo en 0600. Hay que revisar quién puede atravesar los padres, quién puede cambiar la entrada, qué servicio corre con qué identidad, si un atacante puede sustituir un enlace o directorio y qué mecanismos externos comparten la autoridad. El principio de mínimo privilegio se concreta en rutas, operaciones y momentos; no es una etiqueta de permisos.

En respuesta a incidentes, se separan cuatro capas del informe: observación («la entrada existe y apunta al inodo X»), inferencia («un proceso con identidad Y la creó según el log Z»), impacto posible («el servicio puede cargar una configuración distinta en el próximo arranque») y decisión («aislar, preservar o revertir bajo autorización»). Esta separación evita llamar incidente a una alerta o afirmar ejecución sólo por existencia.

En recuperación, la prueba debe incluir un reinicio o fallo simulado dentro del entorno autorizado y comprobar el estado, no sólo el código de retorno. Se documentan versiones, filesystem, dispositivo, política de montaje, errores de sincronización y el resultado de leer desde una instancia nueva. Una restauración correcta también debe verificar propietario, permisos, ACL, etiquetas, referencias y coherencia con los datos asociados.

Síntesis: seguir la transición, no la apariencia

El modelo completo puede leerse como una cadena: una ruta se resuelve en un namespace; una entrada conduce a un inodo; los metadatos describen el objeto; una identidad y una política deciden la operación; open crea una referencia que puede sobrevivir a cambios del nombre; una escritura modifica datos y metadatos; rename reorganiza nombres; fsync y el sistema de almacenamiento determinan qué puede recuperarse después de un fallo.

Cada transición tiene condiciones y límites. Un nombre no prueba identidad del objeto. Un modo no resume toda autorización. Un descriptor no vuelve segura una aplicación que valida el objeto equivocado. Una lectura posterior no prueba durabilidad. Un tiempo o hash no demuestra intención. Un journal o un reemplazo atómico de namespace no define por sí solo la transacción de negocio.

La pregunta profesional, por tanto, no es «¿el archivo está protegido?». Es: «¿qué principal puede ejecutar qué operación sobre qué objeto, a través de qué resolución y bajo qué política; qué estado debe sobrevivir a qué evento; qué evidencia observa cada transición; y qué queda fuera de la conclusión?». Con esas fronteras, archivos y metadatos dejan de ser detalles administrativos y se convierten en un modelo comprobable de autoridad, integridad y recuperación.

Comprobación de comprensión

  1. Descriptor frente a pathname. Una aplicación conserva un descriptor de policy.json mientras otro proceso reemplaza la entrada con rename. ¿Qué puede seguir leyendo y qué compararías? Respuesta modelo: la open file description conserva el objeto abierto; pediría fstat(fd) y compararía con stat(path) usando dispositivo, inodo, versión o hash y procedencia. Límite: no prueba quién creó el objeto.
  2. Permisos de archivo y directorio. Compara r, w y x en un archivo regular y un directorio, e incluye un fallo. Respuesta modelo: en el archivo suelen regir lectura, modificación y solicitud de ejecución; en el directorio, enumeración, modificación de entradas y recorrido. Puede existir r sobre el archivo y fallar por falta de x en un padre. Límite: ACL, MAC, capabilities, namespace y montaje también cuentan.
  3. Enlaces y resolución. ¿Cómo distinguirías un hard link de un symbolic link sin fiarte del nombre? Respuesta modelo: observaría lstat, el destino y la identidad de objeto; dos entradas con el mismo inodo sugieren hard link, mientras el symbolic link tiene objeto propio y contenido de ruta. Límite: la conclusión depende del filesystem y de la ventana de observación.
  4. Visibilidad y durabilidad. Un proceso escribe una configuración y otro la lee de inmediato. ¿Qué falta para afirmar supervivencia a un corte? Respuesta modelo: declarar el fallo, comprobar sincronización del archivo y de la entrada de directorio y verificar recuperación en el filesystem y dispositivo evaluados. Límite: una lectura inmediata sólo prueba visibilidad en ese momento.
  5. Reemplazo temporal. Reconstruye la secuencia con rename y separa sus garantías. Respuesta modelo: temporal en el mismo filesystem, escritura/validación, fsync del archivo, rename, fsync del directorio y verificación de recuperación; rename reorganiza el namespace, pero no promete durabilidad universal ni transacción de negocio. Límite: concurrencia y fallos intermedios requieren política propia.
  6. Crítica de 0644. ¿Por qué «0644 prueba que cualquiera puede leer» es excesivo? Respuesta modelo: faltan recorrido de padres, identidad efectiva y controles como ACL/MAC/capabilities; además el pathname puede resolver otro objeto. Límite: el modo es una observación, no la decisión completa.
  7. Tiempos y hash. Un archivo tiene mtime reciente y hash coincidente. ¿Qué sostienen? Respuesta modelo: apoyan esos bytes y metadatos en la observación documentada. Límite: no prueban autoría, origen, intención ni momento exacto; hace falta correlación de proceso, identidad, reloj y autenticidad.
  8. Triage acotado. ¿Cómo estudiarías una entrada de arranque inesperada? Respuesta modelo: preservaría entrada, objeto, permisos, ACL, target, versión, proceso, identidad, tiempos y logs; contrastaría hipótesis legítimas y no autorizadas con controles sintéticos. Límite: detendría al confirmar semántica y autoridad o el impacto autorizado, sin ejecutar payload ni modificar producción.

Problema de transferencia

Un servicio carga /srv/app/policy.json al arrancar. La aplicación primero llama a stat, después abre la ruta, lee el contenido y registra que la operación tuvo éxito. El directorio es modificable por un grupo de soporte; el servicio corre con una identidad distinta; una actualización escribe un temporal y luego usa rename, pero no documenta sincronización. Durante una revisión se observa un archivo con modo 0640, un symbolic link creado recientemente y un reinicio en el que el servicio recupera una versión anterior.

Construye un análisis acotado. Dibuja verbalmente la resolución de la ruta y sus comprobaciones; identifica qué observaciones pertenecen a namespace, inodo, permisos, descriptor y durabilidad; formula dos hipótesis alternativas para la versión anterior; y pide evidencia que las discrimine. Propón una secuencia de actualización que preserve validación, metadatos esperados y recuperación bajo un modelo de fallo declarado. No atribuyas ejecución maliciosa por la mera presencia del enlace y no declares que rename es suficiente sin revisar filesystem, sincronización, identidad y concurrencia.

Fuentes principales