Flowx: Implementación de no interferencia en Linux

186

Transcript of Flowx: Implementación de no interferencia en Linux

Page 1: Flowx: Implementación de no interferencia en Linux

Flowx:

Implementación de no interferencia en Linux

Maximiliano Cristiá

Pablo Enrique Mata

Facultad de Ciencias Exactas, Ingeniería y Agrimensura

Universidad Nacional de Rosario

Argentina

27 de noviembre de 2009

Page 2: Flowx: Implementación de no interferencia en Linux

Resumen

Esta tesina describe los pasos para la creación de un módulo de seguridad para un sistema operativoLinux, en su versión 2.6, el cual busca imponer políticas de seguridad basadas en un modelo basadoen la no interferencia y el control del �ujo de la información. El objetivo es proteger al sistema tantode caballos de Troya como de ciertas clases de canales encubiertos que los mecanismos de control deacceso tradicionales no pueden evitar.

El módulo se implementará gracias al framework de seguridad que introduce Linux en sus kernels2.6, llamado Linux Security Modules (LSM), el cual provee diferentes hooks en distintos puntos críticosdel kernel, con la intención de que un módulo implemente chequeos, comprobaciones o diferentes tareasa la hora de que cada hook sea invocado.

Como resultado se busca desarrollar un prototipo de seguridad que además de asegurar la con�-dencialidad de la información del sistema, no penalice de manera signi�cativa la performance de éstey sea 100% compatible con el software existente.

Page 3: Flowx: Implementación de no interferencia en Linux

Índice general

1. Introducción 5

2. Seguridad Informática 72.1. ¾Qué es la seguridad informática? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72.2. Políticas de Seguridad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

2.2.1. Control de Acceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82.2.1.1. Control de Acceso Discrecional . . . . . . . . . . . . . . . . . . . . . . 92.2.1.2. Desventajas del Control de Acceso Discrecional . . . . . . . . . . . . . 102.2.1.3. Control de Acceso Mandatorio . . . . . . . . . . . . . . . . . . . . . . 10

2.2.2. Políticas de Soporte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102.3. Trusted Computer Base (TCB) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

3. Modelos de Seguridad 123.1. Seguridad Militar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123.2. Seguridad Multinivel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133.3. Canales Encubiertos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133.4. No Interferencia y Flujo de la Información . . . . . . . . . . . . . . . . . . . . . . . . . 14

3.4.1. Enfoque Estático . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153.4.2. Enfoque Dinámico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

3.5. Modelo de Seguridad de Flowx . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163.5.1. Lenguaje de Flowx . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163.5.2. Máquina Segura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183.5.3. Condición de Seguridad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203.5.4. Cuestiones de Implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

4. Conceptos Avanzados del Sistema Operativo Linux 224.1. Sistemas Operativos y el kernel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224.2. Llamadas al sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234.3. Interrupciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234.4. Linux Security Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244.5. Atributos Extendidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254.6. La Terminal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

4.6.1. La tty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264.6.2. Terminal Controladora . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274.6.3. Archivos Especiales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274.6.4. Job Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274.6.5. Formas de procesar la entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284.6.6. El controlador de la consola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

4.7. Recorrido de rutas de archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304.8. Gestión de archivos de dispositivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

1

Page 4: Flowx: Implementación de no interferencia en Linux

ÍNDICE GENERAL 2

4.8.1. Problemas de la gestión tradicional de dispositivos . . . . . . . . . . . . . . . . 314.8.2. Device Driver Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 324.8.3. Kobjeto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

4.8.3.1. Kobjetos embebidos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344.8.3.2. Inicialización de kobjetos . . . . . . . . . . . . . . . . . . . . . . . . . 354.8.3.3. Conteo de referencias . . . . . . . . . . . . . . . . . . . . . . . . . . . 354.8.3.4. Registrándose en el sysfs . . . . . . . . . . . . . . . . . . . . . . . . . 354.8.3.5. Ktypes y métodos release . . . . . . . . . . . . . . . . . . . . . . . . . 364.8.3.6. Ksets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 364.8.3.7. Subsistemas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

4.8.4. Sysfs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 384.8.4.1. Obtener entradas de sysfs . . . . . . . . . . . . . . . . . . . . . . . . . 394.8.4.2. Habitando el directorio del kobjeto . . . . . . . . . . . . . . . . . . . . 394.8.4.3. Atributos adicionales . . . . . . . . . . . . . . . . . . . . . . . . . . . 404.8.4.4. Links simbólicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41

4.8.5. Udev . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414.9. Identidad de procesos y usuarios y el mecanismo de autenticación . . . . . . . . . . . . 42

4.9.1. La Persona de un proceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 424.9.2. Capacidades de un proceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434.9.3. Init, Getty y Login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44

4.9.3.1. Init . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 444.9.3.2. Getty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 454.9.3.3. Login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46

4.10. Teclas para servicios de seguridad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 484.11. Colas de trabajo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 494.12. Comunicación entre procesos (IPC) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

4.12.1. Pipes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 504.12.1.1. Crear y borrar un pipe . . . . . . . . . . . . . . . . . . . . . . . . . . . 504.12.1.2. Leer y Escribir a un pipe . . . . . . . . . . . . . . . . . . . . . . . . . 51

4.12.2. FIFOs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 514.12.3. System V IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

4.12.3.1. Semáforos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 544.12.3.2. Colas de Mensaje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 554.12.3.3. Memoria Compartida . . . . . . . . . . . . . . . . . . . . . . . . . . . 57

4.12.4. Colas de Mensajes POSIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 584.13. La Memoria en Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60

4.13.1. Manejo de la Tabla de Páginas . . . . . . . . . . . . . . . . . . . . . . . . . . . 614.13.2. TLB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 644.13.3. Memoria Alta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 644.13.4. Espacio de Direcciones de un Proceso . . . . . . . . . . . . . . . . . . . . . . . 65

4.13.4.1. Mapeos en Memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . 674.13.5. Manejador de Excepciones de Fallo de Página . . . . . . . . . . . . . . . . . . . 68

4.13.5.1. Copy On Write . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 694.13.6. Demanda de Páginas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 694.13.7. El Cache de Páginas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70

4.13.7.1. El objeto address_space . . . . . . . . . . . . . . . . . . . . . . . . . . 704.13.8. Reclamo de Páginas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71

4.13.8.1. Swapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 724.13.8.2. Mapeo En Reversa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73

4.14. Kernel Internals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

Page 5: Flowx: Implementación de no interferencia en Linux

ÍNDICE GENERAL 3

4.14.1. Listas del Kernel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 744.14.2. VFS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 764.14.3. El objeto Dentry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77

4.14.3.1. Dentry Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 784.14.4. Bottom Halves . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 784.14.5. Proceso Current . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78

4.14.5.1. Current . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 804.14.6. Cambios de Contexto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80

4.14.6.1. La macro switch_to . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81

5. Lectura y Escritura de archivos 835.1. Los Requerimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83

5.1.1. Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 855.2. Agregar el atributo de seguridad a los procesos . . . . . . . . . . . . . . . . . . . . . . 855.3. Agregar lista de s-hermanos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 865.4. Agregar el atributo de seguridad al inodo de VFS y ext3 . . . . . . . . . . . . . . . . . 87

5.4.1. Usando los xattr . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 875.4.2. El campo i_security del inodo de VFS . . . . . . . . . . . . . . . . . . . . . . . 88

5.5. Modi�car sys_read() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 905.5.1. Duplicar current . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 915.5.2. Modi�car el Program Counter . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94

5.5.2.1. Pensando en el futuro . . . . . . . . . . . . . . . . . . . . . . . . . . . 985.5.3. Asignar el nuevo proceso a otro core . . . . . . . . . . . . . . . . . . . . . . . . 99

5.6. Mo�car sys_write() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1005.7. Evaluación del primer prototipo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101

6. Compartir la entrada estándar entre s-hermanos 1036.1. Analizando y diseñando el requerimiento . . . . . . . . . . . . . . . . . . . . . . . . . . 1036.2. Detalles de la Implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

6.2.1. El Bu�er . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1066.2.2. Las sesiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1106.2.3. Asociando procesos con bu�ers . . . . . . . . . . . . . . . . . . . . . . . . . . . 1106.2.4. La interfaz para task_input_bu�er . . . . . . . . . . . . . . . . . . . . . . . . . 1116.2.5. Simultaneidad en la con�guración del controlador tty . . . . . . . . . . . . . . . 1146.2.6. Acceso no bloqueante a la terminal . . . . . . . . . . . . . . . . . . . . . . . . . 1166.2.7. Limitaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117

7. Intervenciones a más llamadas al sistema 1187.1. Generalización del modelo Flowx . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1197.2. Diseño de los ganchos LSM para este grupo de llamadas . . . . . . . . . . . . . . . . . 1207.3. �owx_�le_permission() y �owx_check_permission() . . . . . . . . . . . . . . . . . . . 121

8. Persistencia de las clases de acceso de los dispositivos 1248.1. Especi�cación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1248.2. Mecanismo para establecer las clases de acceso a los dispositivos . . . . . . . . . . . . . 1258.3. FAQ sobre la implementación del mecanismo . . . . . . . . . . . . . . . . . . . . . . . 127

Page 6: Flowx: Implementación de no interferencia en Linux

ÍNDICE GENERAL 4

9. Usuarios y Autenticación 1329.1. Especi�cación y diseño del mecanismo de autenticación . . . . . . . . . . . . . . . . . . 1329.2. Implementación de �ogin y setuid() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

9.2.1. �ogin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1359.2.2. Setuid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

9.3. Camino Con�able en Flowx . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1389.3.1. Implementación del camino con�able . . . . . . . . . . . . . . . . . . . . . . . . 1399.3.2. Ventana Con�able . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

9.3.2.1. Implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

10.Comunicación Entre Procesos 14810.1. Pipes y FIFOs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148

10.1.1. Diseño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14810.1.2. Implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152

10.2. System V IPC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15510.2.1. Diseño General . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15610.2.2. Implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159

10.2.2.1. Colas de Mensajes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16210.2.2.2. Semáforos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16310.2.2.3. Memoria Compartida . . . . . . . . . . . . . . . . . . . . . . . . . . . 163

10.3. Mapeo de Archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16910.3.1. Diseño . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16910.3.2. Implementación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171

10.4. Colas de Mensajes POSIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174

11.Conclusiones 178

12.Trabajo Futuro 18112.1. Comunicación de Procesos en Red . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18112.2. Sistema de Ventanas X . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18112.3. Canales Encubiertos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182

Page 7: Flowx: Implementación de no interferencia en Linux

Capítulo 1

Introducción

La con�dencialidad de la información de un sistema es quizás uno de los aspectos más estudia-dos dentro de la seguridad informática. Desde hace décadas, diferentes enfoques y modelos han sidopresentados con el �n de solucionar este problema de especial interés en sistemas de uso militar. Eneste contexto, no importa tanto si la información es destruida o alterada, lo importante es que no seadivulgada a fuentes que no deberían tener acceso a ella.

Una de las grandes amenazas de los sistemas contemporáneos, con respecto a la con�dencialidad,son los ataques a través de los denominados caballos de Troya. Estos asumen la identidad de un usuarioespecí�co en la máquina an�triona y transmiten, sin ser percibidos, datos a otros entes que no tendríanacceso a la información en condiciones normales.

Muchos modelos han sido propuestos para remediar este tipo de ataques imponiendo políticas quecontrolen el acceso a la información sensible. El más conocido es el modelo Bell y La Padula [3]. Sinembargo, pocos tenían en cuenta que los sistemas poseen otros tipos de vulnerabilidades que los caballosde Troya no explotan y, que un simple remedio a este tipo de software, no implica que un atacante nopueda esquivar los mecanismos de seguridad de otra manera. Esta manera consiste en explotar canalesde comunicación implícitos que poseen todos los procesos del sistema y son denominados canalesencubiertos o covert channels.

En 2003, Andrei Sabelfeld y Andrew Myers [7], recopilaron un grupo de diversas técnicas, incluyendoalgunas propias, para combatir a este tipo de amenaza. Estas técnicas se basan en el enfoque basadoen el �ujo seguro de la información propuesto por primera vez por Denning [5], el cual consistía en eluso de técnicas en la programación de aplicaciones para especi�car e imponer políticas de information�ow. Ya para ese entonces era totalmente conocida la falla en el mecanismo de control de acceso y supropuesta para reforzar esta falla se centraba en el análisis estático del código para detectar caminosde ejecución malignos que violen alguna noción de no interferencia. Esta propiedad fue introducidapor Goguen y Meseguer [4] en 1982 y sostiene que un sistema veri�ca no interferencia si asegura quela información con�dencial no inter�era con la información pública. El método usual para mostrar quevale la no interferencia es demostrar que un atacante no pueda observar ninguna diferencia entre lasalida de dos ejecuciones de un programa que di�eren solo en su entrada con�dencial.

En 1996, Volpano y otros [6] proponen el primer sistema de tipos para no interferencia y junto con lastécnicas desarrolladas por Sabelfeld se pueden cubrir muchos aspectos para asegurar esta propiedada través del análisis de código en tiempo de compilación. Sin embargo, estas técnicas abarcan unaclase bastante restringida de lenguajes de programación y sistemas de computación, haciendo quela compatibilidad con aplicaciones actuales sea muy difícil. Siguiendo esta idea, Maximiliano Cristiáespeci�có un modelo que controla el �ujo de la información donde se combinan técnicas estáticasy dinámicas para un conjunto general de lenguajes de programación y un sistema operativo de lafamilia UNIX. Este modelo veri�ca propiedades de no interferencia, posee compatibilidad completacon software existente y un muy alto nivel de usabilidad.

Este trabajo presenta una implementación de este último modelo de seguridad [8], el cual sí abarca

5

Page 8: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 1. INTRODUCCIÓN 6

en gran parte, el problema de los canales encubiertos (o por lo menos un gran conjunto de éstos)imponiendo políticas de seguridad basadas en la no interferencia y el control del �ujo de la información.Este trabajo presenta un prototipo, al que hemos denominado Flowx, que implementa sobre Linuxel mencionado modelo. El objetivo es comprobar si se pueden imponer correctamente las políticasde�nidas en el modelo, ver cómo esto afecta a la performance del sistema y si se conserva compatibilidadcon el software.

La implementación se desarrollará para un sistema operativo Linux versión 2.6.21.3 y usará unanueva tecnología de los kernel 2.6, llamada Linux Security Modules (LSM). LSM es un frameworkdiseñado con el objetivo de permitir a diferentes desarrolladores en el mundo implementar sus propiaspolíticas de seguridad como módulos del núcleo. La razón de su existencia es que Linus Torvaldsy el equipo de desarrollo de Linux considera que aún no existe un modelo de seguridad totalmenteconsistente y abarcativo que pueda ser incluido en el árbol de fuentes o�cial del kernel. El frameworkconsiste en invocaciones ubicadas en diferentes partes del código, las cuales median en casi todas lasdecisiones que conciernen con la seguridad, desde permisos de lectura y escritura de archivos hastasubir o bajar la prioridad de ejecución de un proceso dado.

Gracias al LSM y a pequeñas modi�caciones en el núcleo en sí (mínimas, ya que se intentaráexplotar al máximo el framework y no tener que tocar la esencia del sistema), se intentará programarel módulo Flowx, cuyos aspectos principales son:

Intervención de numerosas llamadas al sistema como por ejemplo read, write, open, mknod,unlink, mkdir, etc.

Duplicación de procesos dentro del kernel : introduciendo la noción de �s-hermano�, el cual es unproceso exactamente igual a otro salvo por sus clases de acceso y que el Program Counter (PC)del primero apuntará a la instrucción inmediatamente anterior que la del segundo.

Compartir la entrada de bajo nivel entre s-hermanos: habrá un s-hermano que tenga el controlde la Terminal y toda entrada de esta deberá ser compartida con sus s-hermanos.

Etiquetado de inodos: se usará los atributos extendidos (xattr) estándares para ext3, ext2, nfs yRaiserFS a partir del Linux 2.6.

Etiquetado de procesos: se asignará una clase de seguridad a cada proceso del sistema.

Etiquetado de dispositivos: algunos con diferentes clases de seguridad para entrada como para sal-ida. Uso de SysFS para controlar el cambio dinámico de estás clases a manos de un administradorde seguridad.

Autenticación: a través de caminos seguros (trusted path) y asignación dinámica de terminalescon diferentes niveles de seguridad.

El resto de este informe se divide en capítulos donde se desarrollan los conceptos necesarios paracomprender por completo todos los aspectos del prototipo Flowx. El capítulo 2 presentará el campo dela seguridad informática y los conceptos principales que éste introduce. El capítulo 3, explicará algunosde los modelos que fueron propuestos en el pasado para combatir los ataques a la con�dencialidad delsistema, culminando con una desarrollo detallado del modelo de Cristiá. El capítulo 4 repasará algunosconceptos avanzados del sistema operativo Linux. Los capítulos 5, 6, 7, 8, 9 y 10 introducen detallada-mente cada uno de los mecanismos de seguridad implementados. Aquellos lectores que conozcan a lavez el problema de la no interferencia y el sistema operativo Linux en detalle pueden leer directamentela sección 3.5 y luego los capítulos 5 al 10.

Page 9: Flowx: Implementación de no interferencia en Linux

Capítulo 2

Seguridad Informática

2.1. ¾Qué es la seguridad informática?

El signi�cado del término seguridad informática ha evolucionado en los últimos años. En tiemposanteriores, este problema se enfocaba principalmente en la protección de los componentes físicos de unsistema de computación, tradicionalmente por tres razones:

Prevenir el robo o el daño al hardware.

Prevenir el robo o el daño a la información.

Asegurar la disponibilidad de los servicios del sistema.

Generalmente, procedimientos muy estrictos se llevaban a cabo para aislar las máquinas en cuartosmonitoreados, donde solo aquellos con la autorización necesaria podían acceder. Estos procedimientoseran, usualmente, las únicas medidas que tomaban algunas organizaciones con respecto a su seguridadinformática. Sin embargo, con el advenimiento de las terminales remotas, las comunicaciones o, en sumayor medida, Internet, los cuidados físicos solo proveían protección al hardware en sí, exponiendo alsistema ante ataques que permitirían una libre divulgación de su información privada a través de susconexiones externas.

Los tres aspectos fundamentales a tener en cuenta son:

Con�dencialidad: Prevenir la difusión de información con�dencial.

Integridad: Prevenir la modi�cación de información con�dencial.

Disponibilidad: Prevenir la retención de información sensible o recursos del sistema.

De estos tres puntos, la con�dencialidad y la integridad son los que más preocupación conllevanen el mundo real. Particularmente, los sistemas militares, prestan especial atención en el primero, yaque su principal objetivo es que la información secreta no pueda ser divulgada tras un ataque interno,sin importar (o sí, pero en bastante menor medida) si la información fue modi�cada o destruida.La disponibilidad es una propiedad mucho más difícil de lograr en un sistema. Esto lleva tambiéna que, a diferencia de la con�dencialidad y la integridad, sea imposible determinar si un sistemabrinda exitosamente una protección adecuada contra ataques a la disponibilidad, al ser esta prueba nocomputable.

Debido a esto, decimos que: la seguridad informática es la rama de la ciencia de la computaciónque se enfoca en defender la información de un sistema de las amenazas contra la con�dencialidad yla integridad.

Entender de seguridad informática implica comprender tres nociones fundamentales:

7

Page 10: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 2. SEGURIDAD INFORMÁTICA 8

Una política de seguridad (security policy) que dicta Leyes, reglas y prácticas que regulan comola organización maneja, proteje y distribuye la información sensible.

La funcionalidad de mecanismos internos que imponen la política de seguridad.

Seguridad de que estos mecanismos imponen la política.

2.2. Políticas de Seguridad

En el mundo real, una política de seguridad describe como las personas pueden acceder a documen-tos u otro tipo de información. Para poder re�ejar esta política en ambientes informáticos debemosreescribir estas reglas en términos de objetos y sujetos que tengan signi�cado para las computadoras.Esta correspondencia es clara: los sujetos son las entidades activas en el sistema que operan sobre lainformación en nombre de usuarios del sistema. Los sujetos son procesos ejecutando en un dominioparticular. La mayoría de estos procesos actúan según los deseos de individuos cuya identidad ha sidoautenti�cada a través de una contraseña o algo similar, usando medios de comunicación con�ables entreestos individuos y la porción del sistema que realiza la autenti�cación. Estos medios de comunicacióncon�able son llamados trusted path y serán analizados con más detalle en secciones posteriores.

Los objetos contienen información que los sujetos quieren acceder. En general, podemos de�nir alos objetos como porciones de memoria, archivos, directorios, colas, mensajes interprocesos, paquetesde la red, dispositivos de entrada y salida o cualquier otro tipo de almacenamiento de información.

La política de seguridad consiste, entonces, en un conjunto preciso de reglas para determinar laautorización en las diferentes interacciones sujeto-objetos que se realicen en el sistema. Mientras todossistemas tienen propiedades de seguridad, éstas no siempre son explícitas, y las políticas en que estánbasadas muchas veces son difíciles de deducir. La falta de una política clara es la causa por la cualmuchos controles de seguridad de un sistemas son defectuosos.

Es de suma importancia entender la distinción entre una política de seguridad y los mecanismoscon los cuales se implementa tal política en un sistema dado. Estos mecanismos pueden ser usadosdentro de una computadora para forzar el cumplimiento de una política de seguridad que controlael acceso de personas a la información, pero ninguno de estos es en sí la política. Hay casos en losque para algunas políticas de seguridad hay mecanismos que no son lo su�cientemente robustos paraaplicarlas. Confundir mecanismos con políticas puede crear una falsa ilusión de seguridad en un sistemasin proveer una seguridad real.

Las políticas de seguridad pueden ser dividas en dos clases:

Políticas en el control de acceso. Es la porción de la política de seguridad encargada del controlde acceso.

Políticas de soporte. Es la parte que especi�ca las reglas para asociar humanos con las accionesque los sujetos realizan, como sus embajadores, para acceder a la información controlada.

2.2.1. Control de Acceso

El objetivo primario de los mecanismos de seguridad en un sistema es el control de acceso, el cualconsiste en tres tareas:

Autorización: determinar qué sujetos pueden o tienen derecho a acceder a qué objetos.

Determinar cuales son estos derechos (una combinación de modos de acceso como leer, escribir,ejecutar, borrar, etc).

Permitir que solo se pueda hacer uso de estos derechos de acceso y de ninguno más.

Page 11: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 2. SEGURIDAD INFORMÁTICA 9

Asociado a cada objeto hay un conjunto de atributos de seguridad que ayuda a determinar laautorización y el acceso. Estos atributos pueden ir desde un par de bits indicando los modos de accesoque todos los sujetos tiene sobre el objeto, o pueden ser mucho más complejos, como sería el casode una lista de control de acceso (ACL) conteniendo sujetos individuales y sus diferentes accesos alobjeto. Lo más común, sin embargo, es que los atributos de seguridad sean niveles de seguridad y bitsespecialmente activados o desactivados para media en el acceso.

Algunos sistemas también asignan niveles de seguridad a los sujetos. Estos pueden ser usados paradeterminar los diferentes accesos al objeto gracias a una simple comparación entre los niveles de ambos.

Las políticas de control de acceso se dividen en dos categorías: discrecionales y no discrecionales.Estas dos categorías han sido históricamente consideradas muy necesarias para �nes de seguridad tantocomercial como militar.

2.2.1.1. Control de Acceso Discrecional

Este tipo de control es el más común en sistemas donde la seguridad no es un aspecto fundamentaly se puede encontrar en cualquier máquina de un usuario normal hoy en día. Este control consiste,como bien denota su nombre, en permitir que los propios sujetos en el sistema especi�quen quien tendráacceso a la información a su discreción.

Contraseñas a archivosUn usuario puede acceder a un archivo siempre y cuando ingrese la contraseña correcta para ese

archivo cuando sea solicitada. Esta contraseña es, idealmente, diferente que la contraseña que usó elusuario para autenti�carse en el sistema.

Más allá de su simplicidad, el uso de contraseñas para controlar el acceso a archivos no es del todobuena, debido a los siguientes peligros:

No se puede revocar a un usuario para que no use más el archivo a menos que se cambie lacontraseña, lo que afectará a los usuarios autorizados.

No hay forma de saber quiénes son los que tienen acceso al archivo, ya que las contraseñas sondistribuidas manualmente.

Requerir que un usuario recuerde contraseñas por cada archivo que quiera acceder es un cargapoco razonable.

Lista de CapacidadesOtra forma de control de acceso discrecional es la lista de capacidades (capability list). Una capaci-

dad es un atributo de un objeto especi�co junto con un modo de acceso (leer, escribir, ejecutar). Unsujeto que posea dicha capacidad podrá acceder al objeto con el modo especi�cado.

La desventaja de este enfoque es la cantidad de listas que el sistema debe mantener para cadausuario, detallando qué capacidades tiene para qué objetos.

Propietario/Grupo/OtrosUna forma muy usada y simple de control de acceso es el esquema que presentan los sistemas de la

familia UNIX, en donde cada objeto tiene asignado unos pocos bits conteniendo información de cómocontrolar su acceso:

R W E R W E R W E

Los primeros tres bits pertenecen a los permisos que tiene el propietario del archivo y cada unocorresponde a lectura (Read), escritura (Write) y ejecución (Execution) respectivamente. El segun-do grupo de tres bits simbolizan tales permisos para los integrantes del grupo al que pertenece elpropietario, y �nalmente, los últimos bits se usan para el resto de los usuarios del sistema.

Page 12: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 2. SEGURIDAD INFORMÁTICA 10

La desventaja de este esquema es la imposibilidad de que restringir o privilegiar a un usuarioparticular sobre un objeto dado. Si, por ejemplo, queremos asignar al señor López, perteneciente algrupo del propietario del archivo, otro tipo de permisos diferentes a los que simbolizan la segunda tuplade bits, simplemente no podremos.

Lista de Control de Acceso (ACL)Una de las formas más efectivas de control de acceso desde el punto de vista del usuario es la lista

de control de acceso (Access Control List). Esta lista especi�ca los usuarios individuales o grupos deusuarios que pueden acceder al objeto. Como cada lista es parte del archivo y está guardada en un sololugar, decir quien tiene acceso o no al archivo o modi�car esto es muy sencillo y e�ciente.

2.2.1.2. Desventajas del Control de Acceso Discrecional

La mayor desventaja de cualquiera de estos esquemas y del control de acceso discrecional en sí, esque por su naturaleza es muy susceptible a ataques a través de caballos de Troya.

Un caballo de Troya es un programa que engaña al usuario haciéndose pasar por una aplicaciónbenigna y con una funcionalidad legítima, pero en el fondo desarrolla funciones ilícitas que el usuarionunca tuvo la intención de llevar a cabo. Para mostrar un ejemplo de cómo este tipo de ataques puedenafectar la con�dencialidad de un sistema, pensemos en un caballo de Troya ejecutado por un usuariocon acceso a documentos sensibles. Este programa, en un principio, no representa ningún riesgo parael usuario negligente, actuando de forma transparente y sin dejar rastros sobre su trabajo sucio, perodetrás puede simplemente estar leyendo información sensible asumiendo la identidad del usuario (y enverdad la tiene, ya que éste fue el que lo invocó en primera instancia) y escribiendo o comunicandopor diferentes canales esta información, a lugares donde el atacante tenga acceso, por ejemplo, undocumento de más bajo nivel.

2.2.1.3. Control de Acceso Mandatorio

Las políticas de control de acceso mandatorio previenen algunos ataques de caballos de Troya alimponer restricciones en el acceso que no pueden ser ignoradas ni esquivadas. En estos controles se lesasigna a los sujetos y objetos atributos especiales de seguridad que no pueden ser cambiados fácilmentecomo sucedía con los controles discrecionales, como las ACL. El sistema decide si un sujeto puede teneracceso a un objeto comparando sus atributos de seguridad. Un programa actuando en nombre de unusuario tampoco podrá cambiar su atributo ni el de cualquier objeto, incluyendo aquellos de los cualeses propietario. De esta forma, este tipo de control previene el hecho de divulgar información a través dela creación de un archivo compartido para escribir en éste el contenido de un archivo más con�dencial.

Los controles mandatorios son usados en combinación con los controles discrecionales para obteneruna sólida restricción en el acceso. Estos entran en juego automáticamente como un a forma más fuertede protección ante el mal uso, intencional o accidental de los controles discrecionales.

2.2.2. Políticas de Soporte

Además de la políticas para controlar el acceso (mandatorias y discrecionales), existen requerim-ientos de seguridad relacionados con la identi�cación de los individuos para realizar acciones relevantesa la seguridad en un sistema informático. Estos requerimientos constituyen las políticas de soporte y,a diferencia de las políticas que regulan el acceso, éstas no poseen una teoría que respalde su correctaimplementación. Sin embargo, es posible analizar y testear el software que llevan a cabo las políticasde soporte en un sistema y concluir si funcionan correctamente o no.

Al hablar de políticas de soporte debemos conocer dos aspectos importantes: la identi�cación y laautenticación.

Page 13: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 2. SEGURIDAD INFORMÁTICA 11

Identi�caciónEn resumen se puede decir que la identi�cación brinda una mecanismo para que el sistema pueda

asociar a los sujetos con las identidades de usuarios individuales del mundo real. Ahora vale preguntarse,¾cómo saber si está asociación ha sido efectuada correctamente? La respuesta radica en la autenticación.

Autenti�caciónEsta trata en proveer algún mecanismo al sistema para con�rmar que un usuario es quien dice

ser. Dentro de las formas más usadas para llevar a cabo este trabajo, se encuentran los esquemas decontraseñas, las tarjetas inteligentes, los escáner retinales, etc.

Camino Con�able Sin embargo, no todo es tan simple como parece, ya que existen diversos ataquesposibles para poder engañar al mecanismo de autenticación. Uno de los escenarios más comunes es aquelen que un caballo de Troya asume la identidad del software encargado de proveer la autenticación y,el usuario, sin saber ésto, expone su contraseña al programa revelando la única prueba que exige elsistema para que un el usuario justi�que su identidad.

Si se quiere eliminar la posibilidad de que un ladrón de contraseñas nos juegue este truco se debedesarrollar algún tipo de mecanismo para que el sistema pueda tomar la contraseña pero el ladrón no.Esto, comúnmente, es logrado a través de lo que es llamado un camino con�able o trusted path.

La necesidad del camino con�able radica en el advenimiento creciente de virus o caballos de Troyaque intentan recolectar información personal de una computadora. No es difícil instalar, gracias aldescuido del usuario, programas que imiten perfectamente un login y así robar su contraseña para usosfuturos.

Para implementar un camino con�able se debe proveer un mecanismo que el usuario pueda utilizarpara comunicarse con el sistema de manera segura. El usuario inicia el intercambio de información(ej. nombre y contraseña), pero debe garantizarse que esta acción se debe a una respuesta de la partecon�able del sistema.

En particular, es importante en la noción de trusted path, que no se ejecutará ningún código fuerade la parte con�able del sistema al momento de ejecutar el propio camino con�able.

2.3. Trusted Computer Base (TCB)

Una vez estudiadas y de�nidas nuestras políticas de seguridad, llega la hora de implementarlas.Llamamos Trusted Computer Base (TCB) al subconjunto del sistema operativo encargado de la mecan-ización de las políticas de seguridad. Se desea siempre que la TCB sea una parte relativamente pequeñadel sistema de modo que el atacante pueda construir el resto del sistema y de todas formas la infor-mación permanezca segura.

Formalmente se de�ne TCB como la totalidad de los mecanismos dentro de un sistema informático(incluyendo software, hardware y �rmware) cuya combinación es responsable de imponer una políticade seguridad. Cualquier software fuera de la TCB podría ser malicioso. La habilidad de la TCB enasegurar la seguridad del sistema depende únicamente de los mecanismos dentro de ella y de la correctaentrada de los administradores de seguridad con respecto a parámetros necesarios para imponer laspolíticas de�nidas.

Sin embargo, suele ser riesgoso implementar la política construyendo directamente la TCB pues elsalto de abstracción es demasiado alto. Por lo tanto se recurre a un modelo de seguridad. Especí�ca-mente, el modelo de seguridad permite dar el salto entre la política de seguridad y la TCB real a travésde un paso intermedio: una descripción formal de las funciones que la TCB llevará a cabo. Gracias aestas especi�caciones formales luego se pueden probar propiedades requeridas sobre el sistema y sacarconclusiones respecto a esto.

El capítulo siguiente desarrollará una pequeña reseña de los modelos más conocidos y utilizados,los cuales sentarán la base para explicar el modelo de seguridad en el cual se basa este trabajo.

Page 14: Flowx: Implementación de no interferencia en Linux

Capítulo 3

Modelos de Seguridad

A continuación se procederá con el análisis de la seguridad militar comenzando con el modelo deBell y La Padula. Luego se verá que estos sistemas son especialmente susceptibles a un ingeniosoataque denominado Canal Encubierto y cómo, a través de su estudio, surgen teorías y modelos paralimitar más y más las goteras de un sistemas con respecto a la información sensible que éste posee. Elpropósito de este análisis es proveer un trasfondo teórico e histórico para poder introducir y discutirel modelo de seguridad cuya implementación es el centro de está tesis.

3.1. Seguridad Militar

A mediados de los 60's el Departamento de Defensa de los Estados Unidos decidió que debíadesarrollarse algún método para proteger la información clasi�cada almacenada en computadoras.Hasta ese entonces, no estaba permitido procesar este tipo de información en sistemas donde gente sinpermiso tuviera acceso, dado que ninguna máquina podía brindar un procesamiento seguro a archivoscon datos sensibles.

La política de seguridad militar se basa en clasi�car a toda la información en el sistema y etiquetara todos los usuarios de éste con cierta clase de acceso. Las clases de acceso poseen dos componentes:

Un nivel de seguridad (también llamado nivel de sensibilidad) consistente de un grupo de nombrescomo UNCLASSIFIED, SECRET y TOPSECRET .

Un grupo de una o más categorías (también llamadas compartimientos) consistentes en nombrecomo NATO, o NUCLEAR.

Los niveles de seguridad están linealmente ordenados:

UNCLASIFIED < CONFIDENTIAL < SECRET < TOPSECRET

Las categorías son independientes unas de otras y no están ordenadas. Para obtener acceso a lainformación, una persona debe poseer una clase de acceso cuyo nivel sea superior o igual al nivel de laclase de acceso de dicha información y cuyo conjunto de categorías incluya a todas las categorías de laclase de acceso de la información. Claramente, las categorías sirven para restringir información entrepersonas que poseen el mismo nivel de seguridad. Por ejemplo, un escritor de reportes nucleares connivel de acceso SECRET , sabrá que sus documentos no serán vistos por personas con niveles debajodel suyo. Sin embargo, tampoco quiere que personas con su mismo o mayor nivel integrantes de otrosdepartamentos que no sean el nuclear o puedan relacionarse con éste, tengan acceso a la información.De este modo, sus archivos serán etiquetados también con la categoría NUCLEAR para proteger aúnmás sus contenidos.

12

Page 15: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 3. MODELOS DE SEGURIDAD 13

3.2. Seguridad Multinivel

La seguridad multinivel es una descripción matemática de la política de seguridad militar, de�nidaen una forma que puede ser implementada en una computadora. El término multinivel surge por elhecho de etiquetar tanto a personas como información con diferentes niveles de con�anza y sensibilidad.El primer modelo de seguridad multinivel, conocido como el modelo de Bell y La Padula, de�ne unaserie de términos y conceptos que fueron adoptados por la mayoría de los modelos posteriores.

La seguridad multinivel impone su política de forma mandatoria, no permitiendo que los usuariosdel sistema puedan cambiar las etiquetas de los objetos ni sujetos. Además se basa en dos aspectosfundamentales para proteger la con�dencialidad del sistema: que los usuarios con cierto nivel de seguri-dad no puedan leer información de nivel más alto y, que los usuarios con alto nivel no puedan escribiren documentos con nivel de seguridad inferior al suyo. Este último, es el arma principal para combatiral caballo de Troya.

Para resumir, el modelo de seguridad de Bell y La Padula tiene dos propiedades básicas:

Seguridad simple: Un sujeto solo puede leer un objeto si la clase de acceso de éste domina1 a la delobjeto.

Propiedad de con�namiento2: Un sujeto sólo puede escribir en un objeto si su clase de acceso esla misma o es dominada por la clase de dicho objeto.

De esta forma, la única manera en que un sujeto puede leer y escribir un objeto es si sus clases deacceso son iguales.

Desde el punto de vista de la con�dencialidad, no hay ninguna razón para prohibir una escritura�hacia arriba�. Es decir, permitir a sujetos de nivel inferior modi�car documentos de nivel superior. Aquíes donde entra en juego la integridad. Sin embargo, la mayor preocupación de los sistemas militares esno difundir la información sensible que éste posee. Imponer una política tanto para con�dencialidadcomo integridad es un tanto difícil, por ende, todo el esfuerzo en estos sistemas se centra en la restriccióna la lectura de datos sensibles y no a su modi�cación.

3.3. Canales Encubiertos

El ataque a través del caballo de Troya vulnera la con�dencialidad del sistema a través de lacomunicación ilícita de la información por medio de canales legítimos destinados a la intercomunicaciónentre procesos, como son los archivos, mensajes o la memoria compartida. Como se vio, los controlesmandatorios pueden prevenir este tipo de ataque a través del control de acceso. Sin embargo, existenotros caminos ingeniosos por donde los procesos pueden compartir información unos a otros, y loscuales no son protegidos por los mecanismos que implementan el control de acceso. Este otro tipo decaminos son llamados canales encubiertos (covert channels).

Este tipo de canales es peligroso ya que es imposible predecir la cantidad de información que unprograma �pierde� a través de ellos. Más aún, en la mayoría de los casos los canales encubiertos puedencomponerse aumentando su ancho de banda [9]. El parámetro más importante de un canal encubiertoes su ancho de banda, o sea, la tasa, medida en bits por segundo, a la cual la información puede sertransmitida entre procesos. Cada bit de información del sistema que puede ser modi�cado por unproceso y leído por el otro, es un canal encubierto.

En general hay dos tipos de canales encubiertos: canales de almacenamiento y canales de tiempo.El canal de almacenaje transmite la información desde un proceso de alto nivel a uno de bajo nivelescribiendo datos en ubicaciones especí�cas visibles por el proceso de bajo nivel. Para mostrar unejemplo básico:

1Se dice que una clase de acceso domina a otra si su nivel de seguridad es mayor o igual y su conjunto de categoríasincluye al conjunto de la segunda.

Page 16: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 3. MODELOS DE SEGURIDAD 14

Se tienen dos procesos A y B.

A fue invocado por un usuario SECRET sin saber que se trata de un programa malicioso y B esun proceso perteneciente al atacante.

Ahora A pueden leer tranquilamente el contenido de todos los archivo que el usuario puede accederen el sistema, pero gracias a los controles mandatorios, no puede comunicar esta información através de los canales tradicionales.

Entonces A recurriendo a un simple canal encubierto crea y borra un archivo determinado en unaubicación donde B tenga acceso. Cada vez A lea un bit 1 del archivo sensible creará el archivo ypor cada 0 lo borrará.

B simplemente tendrá que chequear la existencia o no de este archivo periódicamente y así, bita bit, reconstruirá el contenido con�dencial del archivo objetivo.

Como puede observarse, el ejemplo muestra cómo el uso de un canal encubierto de almacenaje, de 1 bitpor segundo de ancho de banda, puede esquivar los controles mandatorios de un sistema para vulnerarla con�dencialidad de éste. También, los canales de almacenaje se pueden basar en los atributos de unobjeto o los recursos compartidos (memoria, sincronización sobre un objetos, etc).

Los canales de tiempo pueden comunicar dos procesos, uno de alto nivel y otro de bajo, al variar eltiempo de algún evento detectable. El proceso alto modula su propio uso de algún recurso del sistema(por ejemplo, el tiempo de CPU) de tal modo que esta manipulación afecte el tiempo real de respuestaobservado por el segundo proceso.

La estrategia fundamental para buscar canales encubiertos es inspeccionar todos los recursos com-partidos del sistema, decidir si alguno puede producir este tipo de canales, y medir su ancho de banda.Luego, se pueden incluir controles para auditar permanentemente estos recursos, potencialmente dañi-nos, y mediar en su uso. Sin embargo, es muy difícil determinar que todas las fallas del sistema hansido encontradas.

En la práctica, los desarrolladores de sistemas han encontrado que es casi imposible eliminar atodos los canales encubiertos. Mientras que no todos los expertos de seguridad concuerdan que loscanales encubiertos son inevitables, todos los productos de seguridad multinivel los contienen. Por lotanto, aún los productos más aprobados y usados, contienen debilidades conocidas.

3.4. No Interferencia y Flujo de la Información

Para el desarrollo de sistemas seguros es crucial que todos los �ujos de información a través de éstesean entendidos, estudiados y analizados. En particular, los canales encubiertos pueden llegar a sermuy dañinos en un sistema que intente mantener un alto nivel de con�dencialidad. La no interferenciaes una técnica matemática que permite que un modelo de seguridad sea analizado para detectar estetipo de �ujo de la información.

El concepto de no interferencia fue introducido por primer vez en el paper de Goguen y Messeguer[4] en 1982. Los autores modelan a una computadora como una máquina con entradas y salidas.Estas entradas y salidas son clasi�cadas como de baja o alta sensibilidad. Una computadora posee lapropiedad de no interferencia, si o solo si, toda secuencia de entradas de baja sensibilidad producirálas mismas salidas de baja sensibilidad, sin importar cuáles fueron las entradas de alta sensibilidad.Citando el artículo:

Un grupo de usuarios, usando un cierto conjunto de comandos, no inter�ere con otro grupode usuarios si lo que el primer grupo realiza con esos comandos no tiene efecto alguno enlo que el segundo grupo puede ver.

Page 17: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 3. MODELOS DE SEGURIDAD 15

A través del artículo los autores describen cómo modelar una política de seguridad multinivel quesatisfaga esta propiedad, llegando a un punto donde a�rman que una política de seguridad es unconjunto aseveraciones de no interferencia.

Finalmente, los autores muestran cómo el �ujo de la información puede ser incluido dentro de esteconcepto más general. Recordar que el control del �ujo de la información (information �ow) es unprocedimiento para asegurar que la información transferida dentro de un sistema no transite desdeobjetos de alto nivel de seguridad a objetos de bajo nivel de seguridad. Este control es vital paraestudiar y combatir a los canales encubiertos.

A la hora de hablar de no interferencia se pueden distinguir dos enfoques básicos con respecto acómo se busca imponer esta propiedad en un sistema. Uno de estos enfoques es el estático, el cualintenta, a través de un análisis estático de los programas, poder especi�car e imponer políticas deseguridad basadas en el �ujo de información.

Otro es el dinámico el cual se construye sobre el monitoreo constante de los servicios que proveeun sistema y en la imposición mandatoria de diversas políticas para asegurar un correcto �ujo de lainformación.

3.4.1. Enfoque Estático

Denning [5] fue la primera en observar que el análisis estático de programas podía ser usado paraeste propósito con alta precisión y bajo costo a nivel de performance. Desde ese entonces, diferentestécnicas han sido propuestas con respecto a este campo.

Uno de los enfoques más estudiados es el de los sistemas de tipos. En éste, básicamente, cadaexpresión en el programa tiene un tipo de seguridad consistente en dos partes, un tipo ordinario, comoint, y una etiqueta que describe cómo el valor será usado. A diferencia de las etiquetas usadas en losmecanismos de control de acceso mandatorio, las marcas son completamente estáticas, o sea, no soncomputadas en tiempo de ejecución. La forma en que estas etiquetas son asignadas y de cómo el chequeode tipos sea llevado a cabo, de�nen una política de seguridad de �ujo de información. La seguridad esimpuesta por el chequeo: el compilador se encarga de leer los nuevos atributos de las expresiones y seasegura de que no haya ninguna violación en el �ujo de información durante la ejecución del programa.

Particularmente en [7], los autores muestran cómo las prácticas comunes de seguridad, como elcontrol de acceso o la encriptación, no tienen la capacidad para imponer políticas de con�dencialidaden un sistema, al no poseer la característica fundamental para lograr esto: poder rastrear los diferentescaminos que toma la información durante la ejecución de dicho sistema. En contraparte, muestrandiversas técnicas usadas en lenguajes de programación para poder aplicar estas políticas.

Los defensores del enfoque estático basan sus argumentos en que el uso de métodos dinámicos,como el monitoreo de llamadas al sistema, es un recurso limitado ya que, por su esencia, las políticasde �ujo de la información no son una propiedad de una sola ejecución; en general, éstas requieren unmonitoreo de todos los caminos posibles que pueda tomar un programa.

3.4.2. Enfoque Dinámico

Obviamente, controles mandatorios puros, como han sido introducidos en secciones anteriores, fallanen asegurar la completa con�dencialidad de un sistema, por ende, técnicas más avanzadas e ingeniosasdeben desarrollarse para prohibir que el uso excesivo de canales encubiertos derrumbe el muro deseguridad impuesto por un control determinado.

Más allá de la corriente a favor de los métodos estáticos y de, por supuesto, sus grandes ventajas,esta tendencia a analizar programas en tiempo de compilación tiene un efecto muy adverso cuando noscentramos en la usabilidad de un sistema. Básicamente, los mecanismos para imponer estáticamentepolíticas de �ujo de información implican reconstruir todo el software existente en el mercado, yaque éste esta programado con lenguajes simples, donde sus desarrolladores nunca tuvieron en cuentala propiedades inherentes a la no interferencia en sus productos. Además, suponiendo que se decide

Page 18: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 3. MODELOS DE SEGURIDAD 16

reimplementar la mayoría de las aplicaciones básicas para ser desarrolladas de esta manera, primerohay que guiar a los programadores para un correcta aplicación de las políticas de seguridad que senecesiten imponer en el sistema, lo que implica necesariamente, una completa instrucción en las teoríasde no interferencia, �ujo de la información y seguridad en general. Estas teorías no son nada sencillas,ya que requieren un considerable conocimiento matemático y un amplio trasfondo en seguridad infor-mática, lo que impone que estos desarrolladores sean doctores o profesionales expertos en la cienciade la computación, algo que el mercado actual no marca como tendencia; simplemente, por los costeseconómicos que semejante grupo de desarrollo puede conllevar.

Esta gran desventaja, lleva a que, a pesar de ser un enfoque atractivo y académicamente muyestudiado, el análisis estático no sea muy popular por sus aplicaciones prácticas.

En el modelo presentado en [8], Cristiá, especi�ca un modelo para imponer un política que cumplala propiedad de no interferencia en un sistema general de tipo UNIX. La sección siguiente explicarácon más detalle este modelo, el cual es el usado para la implementación del prototipo de seguridadFlowx, tema central de esta tesina.

3.5. Modelo de Seguridad de Flowx

Como ya mencionó previamente, las técnicas estáticas para controlar el �ujo de la informaciónestán enfocadas en un grupo restrictivo de lenguajes de programación y, por su naturaleza, hacenque la compatibilidad con software existente sea muy difícil. El modelo de seguridad Flowx busca unenfoque dinámico al problema de la no interferencia, presentando su implementación a través de unlenguaje general dentro de un sistema operativo general de la familia UNIX.

A lo largo de su paper, Cristiá, muestra cómo los mecanismos de control diseñados para ser �insta-lados� en el sistema, cumplen con la propiedad fundamental de la no interferencia: sin importar cuálesfueron las entradas de alto nivel, dos ejecuciones diferentes de un programa de bajo nivel siempre ten-drán la misma visión de su entorno al �nalizar. La palabra ��nalizar� hace hincapié en que el modelotiene en cuenta únicamente programas que terminan, pero es fácilmente extendible para re�ejar el casoen que un programa no lo haga. Para lograr demostrar la propiedad, recurre a la especi�cación de unamáquina genérica y un lenguaje de programación, también genérico, que corra sobre dicha máquina.

A continuación se dividirá la explicación del modelo en tres partes. La primera mostrará los detallesque conciernen al lenguaje, sus construcciones y semántica. Luego se explicará la máquina segura ycómo se encargará de controlar la con�dencialidad de los canales sensibles. Por último, se mostrará cómola máquina impone la no interferencia a través de la formulación de un par de teoremas fundamentales.

3.5.1. Lenguaje de Flowx

El lenguaje contempla punteros, asignaciones, variables, expresiones, tipos básicos, entrada/saliday llamadas al sistema; y su gramática puede observarse en la Figura 3.1.

Como en C, si x es una variable, &x será su dirección de memoria y ∗x el contenido de la variablecuya dirección sea x. De esta forma se pueden ver a las variables como simple contenedoras de númeroso de direcciones de memoria. El símbolo � denota cualquier símbolo binario que pueda contener ellenguaje. Notar la instrucción syscall como parte de una sentencia básica. Esta debería ser cualquierllamada al sistema que prevea el sistema operativo, pero para simpli�car la demostración se tomaron 4llamadas fundamentales, descriptas en el Cuadro 3.1. Cabe destacar que la llamada chinsc() puede serllamada por cualquier usuario, mientra que choutsc() solo por un administrador de seguridad y ambasllamadas deben ser requeridas a través de un trusted path.

La semántica del lenguaje (ver 3.2) está de�nida especi�cando cómo el estado de un programacambia al ejecutar cada sentencia del lenguaje. Formalmente la de�ne la función LS:

LS : Memory → Program→Memory

Page 19: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 3. MODELOS DE SEGURIDAD 17

Expr ::= N | V AR | ∗V AR | &V AR | Expr � Expr

BasicSentence ::=skip | var := Expr | ∗var := Expr | syscall(N, arg1, . . . , argn)

ConditionalSentence ::=if Expr then Program fi | while Expr do Program done

BasicAndConditional ::= BasicSentence | ConditionalSentence

Program ::= BasicAndConditional | Program ; Program

Figura 3.1: Gramática del lenguaje de programación del modelo Flowx.

Nombre System call Funciónread(dev, var) syscall(0, dev, var) Lee un var desde el dispositivo de en-

trada dev

write(dev, expr) syscall(1, dev, expr) Escribe expr en el dispositivo de salidadev

chinsc(dev, lev) syscall(2, dev, lev) El SO clasi�cará la próxima lectura devcon nivel lev hasta que chinsc sea eje-cutado nuevamente sobre el dispositivo

choutsc(dev, lev) syscall(3, dev, lev) El máximo nivel de información quepuede salir a través de dev debe sermenor a lev

Cuadro 3.1: Las cuatro llamadas al sistema.

donde Memory es una función de V AR a N representando el estado de la memoria del proceso.

LS(M, skip) = M (LS-skip)

LS(M, x := e) = M ⊕ {x 7→ eval(M, e)} (LS- := )

LS(M, ∗x := e) = M ⊕ {−−→x, M 7→ eval(M, e)} (LS-∗)

LS(M, if e then P fi) = if eval(M, e) then LS(M,P ) else M (LS-if )

LS(M,while e do P done) =if eval(M, e) then LS(M,P ; while e do P done) else M

(LS-while )

LS(M,P1 ; P2) = LS(LS(M,P1), P2) (LS- ; )

Figura 3.2: Semántica del lenguaje. M ∈Memory, x ∈ V AR, e ∈ Expr.

Como el lenguaje incluye punteros se asume que el sistema almacena internamente una funciónbiyectiva, denominada addr, la cual retorna la variable almacenada en una dirección dada. Entonces,addr : N � V AR. Además, se asume que esta función será la misma para todas las ejecuciones delmismo programa. Si x es una variable del programa y M ∈ Memory, entonces

−−→x, M será igual a

addr ◦M(x).Las expresiones son evaluadas a través de una función llamada eval, la cual toma un elemento de

Memory y una expresión y retorna un número natural, como muestra la Figura 3.3.

Page 20: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 3. MODELOS DE SEGURIDAD 18

eval(M, n) = n (eval-N)eval(M,x) = M(x) (eval-V AR)

eval(M, ∗x) = M(−−→x, M) (eval-∗)

eval(M,&x) = addr−1(x) (eval-&)

eval(M, e1 � e2) = eval(M, e1)� eval(M, e2) (eval-�)

Figura 3.3: Expression evaluation. M ∈Memory, n ∈ N, x ∈ V AR, e1, e2 ∈ Expr.

3.5.2. Máquina Segura

Los programas desarrollados con el lenguaje especi�cado correrán sobre una máquina genéricay segura, SM (Security Machine), la cual será la encargada de controlar el �ujo de la informacióndurante la ejecución. Se asume que SM se encontrará en el medio del usuario y del hardware, teniendola habilidad de inspeccionar la estructura de cada programa provisto por el usuario y decidiendo sipermite a este software interactuar con su entorno o no. La función SM está de�nida a continuación:

SM :SState× Env ×Memory × Program

→ SState× Env ×Memory

donde SState y Env están de�nidas por:

DEV ::= il | ih | ol | ohLEV EL ::= L | H

SState , [m : Memory, dl : DEV → LEV EL]Env == DEV → seq N

Por simplicidad se asume que hay solo dos niveles posibles para los dispositivos de entrada, peroel modelo se puede ampliar fácilmente para soportar más niveles. Además también se supone que hayun solo dispositivo por cada tipo de entrada y salida.

SState modela el contexto del programa ejecutado y los niveles en que cada dispositivo está traba-jando: m representa la memoria donde los valores de bajo nivel son guardados y dl el nivel de seguridadactual de cada dispositivo. Por su parte, Env simboliza el conjunto de todos los dispositivos junto conuna secuencia de número naturales con diferente signi�cado dependiendo el tipo: información todavíano procesada para los canales de entrada (il,ih) o información ya enviada al ambiente para los canalesde salida (ol, oh).

Las transiciones de SM se pueden observar en las Figuras 3.4 y 3.5, y nuevamente, para no complicarel modelo, no se han incluido transiciones para cambiar la clase de acceso de los dispositivos. Notar queel modelo permite que los usuarios de bajo nivel modi�quen el entorno de los de alta clase de acceso,pero cabe recordar, que el objetivo de este trabajo es conservar la con�dencialidad del sistema y no suintegridad.

La complejidad del modelo y de la no interferencia en si, surge al analizar los diferentes caminos queun programa puede seguir al evaluar expresiones que contengan diferentes sensibilidades. Las reglasSM-if and SM-while son las encargadas de manejar está lógica y, por ende, las fundamentales parala validez del modelo. SM-write(ol) asegura que la salida de un dispositivo low provendrá únicamentedesde la memoria low. Además, si una sentencia del programa tiene la forma if a = 0 then write(ol, 1) fi,donde a almacena un valor high, entonces la regla SM-if estipula que la llamada write() debe ser salteadao que no debe producir salida alguna. Esto no implica necesariamente que las salidas de bajo nivelserán �bu�ereadas� hasta que el programa termine como puede hacer pensar la de�nición del tercer

Page 21: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 3. MODELOS DE SEGURIDAD 19

caso. De hecho, el caso dice que si la sentencia más profunda de la estructura if debe ser ejecutadasolo sobre M , entonces solo la salida de alto nivel será producida. Esta regla no implica que los efectoslow son desechados, ya que no habrá ningún efecto a desechar, debido a que eval(S.m, e) arroja falsey, entonces, el �thread� corriendo sobre S.m no ejecutará la sentencia más interna.

SM : SState× Env ×Memory × Program→ SState× Env ×Memory

SM(S, E, M, skip) = (S, E, M) (SM-skip)

SM(S, E, M, x := expr) =([m← LS(S.m, x := expr), dl← S.dl], E, LS(M, x := expr))

(SM- := )

SM(S, E, M, ∗x := expr) =([m← LS(S.m, ∗x := expr), dl← S.dl], E, LS(M, ∗x := expr))

(SM-∗)

SM(S, E, M, read(il, x)) =([m← S.m⊕ {x 7→ head ◦ E(il)}, dl← S.dl],E ⊕ {il 7→ tail ◦ E(il)}, M ⊕ {x 7→ head ◦ E(il)})

(SM-read(il))

SM(S, E, M, read(ih, x)) =(S, E ⊕ {ih 7→ tail ◦ E(ih)}, M ⊕ {x 7→ head ◦ E(ih)})

(SM-read(ih))

SM(S, E, M, write(ol, e)) =

(S, E ⊕ {ol 7→ 〈eval(S.m, e)〉 _E(ol)}, M)

(SM-write(ol))

SM(S, E, M, write(oh, e)) =

(S, E ⊕ {oh 7→ 〈eval(M, e)〉 _E(oh)}, M)

(SM-write(oh))

SM(S, E, M, if e then P fi) =

SM(S, E, M,P ) if eval(S.m, e) ∧ eval(M, e)(SM(S, E, M, P ),1,SM(S, E, M,P ),2, M)

if eval(S.m, e) ∧ ¬eval(M, e)

(S, E′, SM(S, E, M,P ),3) if ¬eval(S.m, e) ∧ eval(M, e)(S, E, M) if ¬eval(S.m, e) ∧ ¬eval(M, e)

where E′ = E ⊕ {ih 7→ SM(S, E, M,P ),2(ih),oh 7→ SM(S, E, M, P ),2(oh)}

(SM-if )

SM(S, E, M, P1 ; P2) = SM(SM(S, E, M, P1), P2) (SM- ; )

Figura 3.4: Semántica de la Security Machine (parte 1).

Page 22: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 3. MODELOS DE SEGURIDAD 20

Let PWhile be P ; while e do P done

SM(S, E, M, while e do P done) =

SM(S, E, M,PWhile) if eval(S.m, e) ∧ eval(M, e)(SM(S, E, M,PWhile),1,SM(S, E, M,PWhile),2, M)

if eval(S.m, e) ∧ ¬eval(M, e)

(S, E′, SM(S, E, M,PWhile),3) if ¬eval(S.m, e) ∧ eval(M, e)(S, E, M) if ¬eval(S.m, e) ∧ ¬eval(M, e)

where E′ = E⊕{ih 7→ SM(S, E, M,PWhile),2(ih),oh 7→ SM(S, E, M,PWhile),2(oh)}

(SM-while )

Figura 3.5: Semántica de la Security Machine (parte 2).

3.5.3. Condición de Seguridad

El siguiente teorema determina que la propiedad de no interferencia es veri�cada en la combinaciónde SM y LS. La propiedad es relevante si hay actualmente dispositivos de alto y bajo nivel, entoncesse de�ne IO como {il 7→ L, ol 7→ L, ih 7→ H, oh 7→ H}.

Suposición 1 (Programas bien de�nidos) Todo loop dentro de cualquier programa a ser ejecutadoes un while bien fundado.

Teorema 1 (Noninterference)

∀S ∈ SState; E1, E2 ∈ Env; M1, M2 ∈Memory; P ∈ Program •P veri�ca la Suposición 1 ∧ S.dl = IO

∧E1(il) = E2(il) ∧ E1(ol) = E2(ol)∧SM(S, E1, M1, P ) = (S′1, E

′1, M

′1) ∧ SM(S, E2, M2, P ) = (S′2, E

′2, M

′2)

=⇒ E′1(ol) = E′2(ol)

La prueba del teorema puede encontrarse en [8] y por cuestiones de brevedad no será incluida aquí.

3.5.4. Cuestiones de Implementación

El objetivo del modelo es resolver un problema práctico, por lo tanto, al implementar SM sobreun sistema operativo existente se deberían tener en cuenta los siguientes aspectos;

Usabilidad El sistema debería imponer restricciones solo cuando la seguridad se vea atentada.

Compatibilidad Los cambios en el sistema para imponer la política de seguridad no deberían in�uiren el normal funcionamiento del software existente.

Performance A pesar de que las organizaciones que requieren este tipo de sistema operativo puedenobtener tecnología más avanzada y rápida, una implementación pobre puede provocar que algunosusuarios pre�eran sistemas inseguros.

La estrategia general de implementación sobre un sistema UNIX es duplicar el proceso que corraun programa inseguro en dos �threads�, cada uno con sus propios descriptores y su propio espacio dememoria, la cual no tendría que solaparse debido a que no tendrían que compartir ningún tipo de datoalojada en ésta. Cada thread debería tener una clase de acceso diferente, simbolizando las operaciones

Page 23: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 3. MODELOS DE SEGURIDAD 21

que puede o no realizar con su entorno y las llamadas al sistema como sys_read() o sys_write() tendríanque tener en cuenta estás �etiquetas� para permitir o no realizar sus funciones.

En cuestiones de performance, si se cuenta con un procesador multicore, cada proceso duplicadopodría ser asignado a un core diferente al cual pertenece en primera instancia el thread original,permitiendo así, descongestionar, en cierta medida, las colas de ejecución de cada micro al momentode correr el programa.

La llamada read debería modi�carse para que dos procesos duplicados compartan las entradas delos dispositivos que constituyen su entorno; principalmente por dos razones:

Preservar la compatibilidad del programa. Por ejemplo, un editor de textos tendría que recibirnormalmente su entrada sin conocer el �ltro que el kernel realizó en el camino antes de enviársela.

Proveer una noción de transparencia a los usuarios, ya que estos en ningún momento deberíanser conscientes de que está actuando el modelo de seguridad, o mejor dicho, su implementación.Supongamos el caso en que un editor de texto debe ser duplicado para manejar información dediferente sensibilidad, resultaría incomodo y poco amistoso para el usuario pedirle a éste queingrese por teclado primero entradas para un editor y luego para el otro.

En el capítulo que concierne a la modi�cación de Linux para implementar el modelo se discuten conmás detalles las decisiones principales para la implementación del modelo, así como otras necesariasque surgen a medida que la teoría se va transformando progresivamente en práctica.

Toda la implementación se hará sobre un módulo del sistema operativo Linux usando las facili-dades que provee la tecnología LSM para desarrollar módulos de seguridad sobre el núcleo del sistema.Debido a la esencia de algunos de los problemas que tendremos que tratar para el desarrollo de laimplementación tendremos que modi�car el núcleo en sí y también, la forma de invocar ciertas apli-caciones para lograr la compatibilidad con el software existente. Sin embargo, trataremos de que estasmodi�caciones sean mínimas, intentando explotar al máximo las funcionalidades que LSM ofrece ysiempre tratando de mantener la TCB lo más reducida posible.

Page 24: Flowx: Implementación de no interferencia en Linux

Capítulo 4

Conceptos Avanzados del SistemaOperativo Linux

Aquí se explicarán algunas nociones avanzadas para comprender el sistema operativo Linux. Todoslos temas incluidos en este capítulo serán de utilidad en los capítulos siguientes que tratan sobre el dis-eño e implementación del modelo Flowx. El lector puede optar por saltear este capítulo, tanto si conocelos temas aquí desarrollados como si pre�ere leer cada tópico en el momento que sea necesario paracomprender el diseño y la implementación de Flowx. Hemos tenido el cuidado de poner las referenciasadecuadas a las secciones de este capítulo, a lo largo de todos los capítulos que le siguen.

4.1. Sistemas Operativos y el kernel

Debido a la gran cantidad de nuevas funciones de los sistemas operativos comerciales actuales,la noción de qué es realmente un sistema operativo es vaga. Muchos usuarios consideran que todolo que ven en la pantalla es el sistema operativo. Sin embargo, el sistema operativo es consideradocomo las partes del sistema responsable del uso básico y la administración. Esto incluye al núcleo y losmanejadores de dispositivos, el boot loader, el interprete de comandos y los archivos básicos y utilidadesdel sistema. Es lo que realmente se necesita para el funcionamiento del sistema y, claramente, esto noincluye un procesador de texto o un reproductor de mp3. El término sistema se re�ere al sistemaoperativo y todas las aplicaciones que corren sobre él.

Así como la interfaz al usuario constituye la parte más externa del sistema operativo, el kerneles la más interna. Es el núcleo del sistema, o sea, el software que provee los servicios básicos paratodas las demás partes, controla el hardware subyacente y distribuye los recursos del sistema. Típicoscomponentes del kernel son los manejadores de interrupciones para servir pedidos de interrupción, elprogramador (scheduler) para poder compartir el CPU entre muchos procesos a la vez, un administradorde la memoria para manejar el espacio de direcciones de los procesos y servicios del sistema como elacceso a la red o la comunicación entre procesos.

En sistemas operativos modernos con unidades protegidas de acceso a la memoria, el núcleo resideen un anillo más protegido del sistema comparado con los programas de usuario. Esto signi�ca espaciode memoria protegido y acceso total al hardware. Este estado es comúnmente denominado kernel-space (espacio del kernel). En cambio, la aplicaciones de usuario se ejecutan en user-space (espacio deusuario). Estas ven un subconjunto de los recursos de la máquina y no pueden realizar ciertas funciones,como un acceso directo al hardware.

22

Page 25: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 23

4.2. Llamadas al sistema

Las aplicaciones corriendo en el sistema puede comunicarse con el kernel a través de diversasllamadas al sistema (system calls), las cuales constituyen la interfaz de los servicios que el núcleoprovee al espacio de usuario. Los programas normalmente llaman a funciones de una librería, porejemplo, la librería estándar C, quien a su vez se apoya en la interfaz de llamadas al sistema paraque lleve a cabo las tareas en su nombre. Cuando una aplicación ejecuta una syscall1 se dice que elnúcleo está ejecutando en nombre de la aplicación. Es más, se dice que la aplicación está ejecutandouna llamada al sistema en espacio kernel y por lo tanto, el sistema se encuentra en kernel mode (modokernel). Caso contrario, es cuando los programas de usuario están ejecutando en espacio de usuario, elsistema se encuentra en user mode (modo usuario). Además, al momento de servir llamadas al sistemase dice que el núcleo se encuentra en contexto de proceso (process context). La �gura 4.1 muestra larelación entre los diferentes espacios y el hardware.

Figura 4.1: Relación entre aplicaciones, kernel y el hardware.

4.3. Interrupciones

Como ya se dijo, el kernel también maneja el hardware del sistema. La gran mayoría de arquitec-turas, especialmente la i386, usada en éste trabajo, soporta el concepto de interrupción. Cuando elhardware se quiere comunicar con el sistema, provoca un interrupción que asíncronamente detiene alkernel. Las interrupciones son identi�cadas por un número. El kernel usa este número para ejecutarun manejador de interrupción especí�co para poder procesar y responder la interrupción. Por ejemplo,cuando se presiona una tecla en el teclado, éste genera una interrupción para avisar de una nueva en-trada en el bu�er del teclado. El kernel nota tal evento y ejecuta el manejador asociado. Para proveersincronización, el kernel puede normalmente deshabilitar a todas las interrupciones o alguna especí�ca.En Linux, los manejadores no corren en process context. En cambio, lo hacen en un contexto especial,

1Forma abreviada de referirse a llamadas al sistema o SYStem CALLS.

Page 26: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 24

interrupt context, que no está asociado a ningún proceso. Este contexto existe para que el manejadorpueda única y rápidamente contestar la interrupción y luego salir.

Los contextos discutidos representan la totalidad de las actividades del kernel. En Linux ser puedegeneralizar que cada procesador está haciendo tres cosas en cada momento dado:

En espacio kernel, en process context, ejecutando en nombre de un proceso especí�co.

En espacio kernel, en interrupt context, no asociado a un proceso y manejando una interrupción.

En espacio de usuario, ejecutando código de usuario en un proceso.

4.4. Linux Security Modules

Una de las cuestiones a resolver en la implementación de Flowx fue si se utilizaba o no el frameworkLSM (Linux Security Modules) que implementa Linux a partir de su versión 2.6. Este framework surgecomo una solución a la problemática de saber cuándo un modelo de seguridad es mejor que otro ycuál es la mejor solución para disminuir las vulnerabilidades de un sistema. Debido a esto, el proyectoLSM desarrolló un framework de propósito general para el control de acceso, el cual permite cargar ydescargar modelos de seguridad mejorados a través de módulos. La ventaja de poder desarrollar estosmódulos es no tener que parchear el kernel con modi�caciones no estándares, de las cuales, por la faltade consenso para encontrar �el modelo de seguridad�, hay numerosas.

En el proyecto Flowx se decidió usar este framework para implementar el control de acceso. Laprincipal razón de esta elección es la facilidad de mantenimiento de un único módulo. Parchear el kernelimplicaría actualizar constantemente nuestras modi�caciones para adaptarlas a las nuevas versiones deéste. El uso de un módulo, junto con este framework, implica que la cantidad de código a manteneres menor y menos compleja, ya que lo inherente al kernel es mantenido por el proyecto LSM. Ademástoda la implementación quedaría concentrada en un solo lugar, lo que implica no tener que recorrertodo el kernel para descubrir los controles impuestos por el modelo. Otra ventaja al usar el frameworkes la transparencia y prolijidad en la implementación del prototipo.

Por último, implementar nuestra política como módulo favorece a la rapidez de desarrollo y a lafacilidad para actualizarlo, ya que no tenemos que compilar todo el kernel por cada modi�cación onueva característica agregada.

Como se explicará más adelante, LSM posee un gran número de hooks2 esparcidos dentro delkernel con el propósito de permitir al módulo mediar en la toma de decisiones antes de crear objetoso estructuras del núcleo, acceder a datos o archivos, montar o crear dispositivos, etc. Estos hooks seencuentran, cada uno, en un lugar �jo y los respalda un estudio del por qué de su existencia y de suubicación. Al reducir la mayoría de nuestro código en implementaciones concretas de algunos de estoshooks, no solo ahorramos tiempo en decidir los lugares para nuestros cambios, sino también, dejamosintacto el kernel, favoreciendo la lectura y prolijidad de este así como también la de nuestro módulo.Algunas de las ubicaciones más importantes de estos hooks son:

Hooks en los procesos: permiten mediar en la creación y destrucción de estos. Además permitenagregar un nuevo campo en la task_struct para poder etiquetar los procesos con los atributos deseguridad necesarios para el modelo.

Hooks en la ejecución de programas: mediadores el proceso de ejecución de nuevos programas.

Hooks en el superbloque: median el montaje y desmontaje y demás operaciones principales que sepueden realizar sobre un superbloque. También permite agregar un nuevo campo a la estructuraque representa al superbloque en el kernel.

2Un hook es un lugar dentro del código que permite a los programadores insertar código propio a medida. Normalmentese usan para agregar mejoras o funcionalidades extras a cierta función.

Page 27: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 25

Hooks en los inodos: similar a los hooks del superbloque pero con las operaciones relativas a losinodos, como la creación, renombramiento, destrucción, etc.

Hooks en archivos (�les): como en los inodos pero en operaciones como la lectura, escritura omapeo en memoria. El hook �le_permission() es de nuestro particular interés ya que es llamadopor el VFS antes de permitir la lectura o escritura a un cualquier archivo.

Hooks varios: otro tipo de hooks útiles para controlar otras componentes importantes en el kernely vitales para la seguridad en muchos modelos, como el manejo de IPCs3, manejo de componentesy protocolos de la red, carga y descarga de otros módulos, etc.

Sin embargo, no todo es color de rosa y no todo el código del prototipo se ubicó dentro de un módulo.Hubo que agregar código y modi�caciones al kernel debido a la naturaleza de las modi�caciones en sí ya qué muchas funciones del núcleo, muy útiles para nosotros, no son exportadas para que los móduloslas aprovechen. Aunque esto complicó un poco nuestra idea inicial, concluimos que la di�cultad enla implementación, sin tocar el kernel, se incrementaba demasiado o se hacía casi imposible, por loque preferimos usar este recurso pero lo mínimo posible para poder seguir gozando de las ventajasmencionadas arriba.

Por ejemplo, un hook típico es el �le_permission(). Este se encuentra en las llamadas al sistemasys_read() y sys_write. Por defecto, este hook siempre devuelve el valor 0, indicando que el procesotiene permisos para realizar la operación. Sin embargo, un módulo puede registrarse y desregistrarse delLSM a través de un par de rutinas denominadas register_security() y unregister_security(), respectiva-mente. Al registrarse, el módulo debe proveer una instancia propia de la estructura security_operationsdonde se encuentran las declaraciones de cada hook. A partir de entonces, cada vez que un hook seainvocado en el kernel, se invocará a la implementación del hook que provea el módulo. Si el módulo noprovee una para un hook en particular, se asumirá el comportamiento por defecto.

Para más información sobre LSM ver [11].

4.5. Atributos Extendidos

El formato del inodo del ext3 es una clase de �chaleco de fuerza� para los diseñadores de sistemasde archivos. El tamaño del inodo debe ser una potencia de 2 para evitar problemas de fragmentacióninterna en los bloques que almacenan la tabla de inodos. Actualmente, la mayoría de los 128 caracteresde un inodo ext3 contienen información y queda muy poco espacio disponible para campos adicionales.Del otro lado, expandir el tamaño del inodo para que sea de 256 sería desperdiciar demasiado espacio,además de introducir problemas de compatibilidad entre sistemas de archivos ext3 que usen diferentestamaños de inodo.

Con la llegada del kernel 2.6 se introdujo en ext3, ext2, xfs, entre otros, los llamados xattr (atributosextendidos). Estos permiten agregar atributos dinámicamente a un inodo sin ninguna modi�cación ensu estructura. Los nuevos atributos son almacenados en un bloque aparte al del inodo en cuestión, elcual únicamente contiene la dirección del bloque extendido en el campo i_�le_acl.

Cada xattr tendrá un nombre y un valor. Ambos estarán codi�cados como un arreglo de caracteresde largo variable, como especi�ca el descriptor ext3_xattr_entry de�nido en fs/ext3/xattr.h. Cadaatributo estará dividido en dos partes: el descriptor ext3_xattr_entry, junto con el nombre del atributo,estarán ubicados al comienzo del bloque; mientras que el valor del atributo estará ubicado al �nal delbloque.

Hay varias llamadas al sistema útiles para asignar, consultar, listar y remover los atributos exten-didos de un archivo. Las llamadas setxattr(), lsetxattr() y fsetxattr() asignan un valor a un atributoextendido de un archivo y esencialmente di�eren en cómo los links simbólicos son manejados y en cómo

3InterProcess Communication.

Page 28: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 26

el archivo es especi�cado (pasando una ruta o un descriptor de archivo). De igual manera, getxattr(),lgetxattr() y fgetxattr() retornan el valor de un xattr ; removexattr(), lremovexattr() y fremovexattr()lo remueven; y listxattr(), llistxattr() y �istxattr() listan todos los atributos extendidos de un archivo(solo los nombres, no los valores).

4.6. La Terminal

4.6.1. La tty

Un dispositivo tty toma su nombre de la antigua abreviación de teletypewriter y fue originalmenteasociada solo con la conexión terminal física o virtual a una máquina Unix. A través del tiempo, elnombre también llego a describir a cualquier dispositivo de puerto serial. Los dispositivos virtuales ttysoportan consolas virtuales que son usadas para ingresar a una computadora, sea por teclado, por unaconexión de red o a través de una sesión xterm. La comunicación entre los dispositivos de terminalesy los programas que leen o escriben en ellos es controlada por la interfaz tty. La capa que soporta estafunción se denomina subsistema tty y es responsable de:

Controlar el �ujo físico de la información a través de líneas asíncronas (incluyendo velocidad detransmisión, tamaño del carácter, disponibilidad de la línea).

Interpretar la información reconociendo caracteres especiales y adaptándose a lenguajes na-cionales.

Controlar los trabajos y los accesos a la terminal usando el concepto de terminal controladora(controlling terminal).

Una terminal controladora maneja las operaciones de entrada y salida de un grupo de procesos.El archivo especial de la tty (creado a través de la llamada sys_mknod()) soporta la interfaz de laterminal controladora. Estos dos conceptos se detallan en las secciones 4.6.2 y 4.6.3

Para realizar estas tareas, el subsistema tty está compuesto por diferentes partes, las cuales poseenun conjunto de reglas de procesamiento que gobiernan la interfaz para la comunicación entre la com-putadora y el dispositivo asíncrono.

Los tres componentes principales de la tty son:

El núcleo de la tty.

El driver de la tty.

Las disciplinas de línea.

El driver tty de Linux vive inmediatamente arriba del driver estándar para dispositivos de caracteresy posee un rango de funciones para proveer una interfaz que todos los dispositivos de terminales puedenusar. El núcleo de la tty es responsable de controlar el �ujo y el formato de la información que pasaa través del dispositivo tty. Esto permite al driver enfocarse nada más que en transferir los datosdesde y hacia el hardware, en vez de preocuparse por la interacción con el espacio de usuario enforma consistente. Para controlar el �ujo de la información, hay un número de �disciplinas de línea�(line disciplines) que pueden ser virtualmente �enchufadas� a un dispositivo tty. Concretamente, lasdisciplinas de línea reside una capa por sobre el driver de la terminal y de�nen su conducta. Ladisciplinas son las responsables de procesar la información y transferirla entre el espacio de kernel y elespacio de usuario.

El núcleo de la tty toma, desde el usuario, los datos que deben ser pasados al driver, pero primeroenvía la información a la disciplina de línea, quien después la pasa al driver. Lo opuesto sucede a lahora de recibir información del dispositivo.

Page 29: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 27

4.6.2. Terminal Controladora

Para cada proceso, el archivo especial /dev/tty es un sinónimo de la terminal controladora asociadaa ese proceso. Dirigiendo mensajes al archivo tty, programas y secuencias del shell pueden asegurarseque estos mensajes son escritos en la terminal aun cuando su salida estándar haya sido redirigida. Losprogramas también pueden redirigir su salida a este archivo para que no sea necesario identi�car laterminal activa.

Uno de los atributos de un proceso es su terminal controladora. Procesos hijos creados por sys_-fork() heredan la terminal de su padre. De este modo, todos los procesos en una sesión heredan laterminal controladora del líder de la sesión. El líder de la sesión que posee una terminal es llamado elproceso controlador de esa terminal.

Solo un grupo a la vez puede tener acceso a la terminal controladora dentro de una sesión y cualquierotro integrante de un grupo que quiera acceder se le enviará inmediatamente la señal SIGTSTP paraparar su ejecución.

4.6.3. Archivos Especiales

A la hora de escribir datos a la terminal, un proceso efectúa una llamada write() sobre el descriptorde archivo de ésta, el cual, normalmente, es inicializado en la llamada fork() cuando se con�gura lasalida estándar. Este archivo, en la mayoría de los casos, se encuentra en el directorio /dev y es productode una llamada sys_mknod(). sys_mknod() llama a la función mknod() de más bajo nivel asociadaal sistema de archivos en donde se quiera crear el archivo especial. Particularmente, como /dev es unsistema de archivos en memoria, se llamará a ramfs_mknod(), ubicada en fs/ramfs/inode.c.

ramfs_mknod(), a su vez, solicita la inicialización de un nuevo inodo para asociárselo al archivoa crear. La función ramfs_get_inode() que se encarga de esta tarea, chequea si el inodo a crearcorresponde a un archivo especial, y de serlo, invoca a init_special_inode() (en fs/inode.c) para hacerque la variable inode->i_fop apunte a dev_chr_fops, dev_blk_fops, dev_�fo_fops o dev_sock_fopssegún de qué tipo de archivo especial se trate. Esta estructura solo contiene un hook a la funciónopen, la cual se implementa con chrdev_open() para dispositivos de caracteres. El propósito de estoes que el resto de las operaciones para manejar archivos (de�nidas en la estructura �le_operations eninclude/linux/fs.h), como son read, write, mkdir, entre otras, sean inicializadas por el driver especí�coque soportará al dispositivo.

A partir de ahora, las llamadas sys_read() y sys_write() serán servidas por las read() y write()correspondientes al driver particular del dispositivo accedido, en nuestro caso; el driver tty.

4.6.4. Job Control

Cada proceso pertenece a un grupo de procesos. Cuando un proceso es creado, se convierte enmiembro del mismo grupo y sesión que su padre. Uno puede ubicar un proceso en otro grupo usandola función setpgid(), siempre y cuando el nuevo grupo pertenezca a la misma sesión.

La única forma de poner a un proceso en otra sesión es convirtiéndolo en el proceso inicial de unanueva sesión, o líder de está, usando la función setsid(). Ambas funciones mencionadas son simplesenvoltorios o wrappers de la libc que llaman a sys_setpgid() y sys_setsid(), en kernel/sys.c.

Normalmente, nuevas sesiones son creadas por el programa de usuario login y el líder es aquel quecorre el intérprete de comandos.

Un intérprete que soporte control de trabajos (job control4) se debe arreglar para controlar quétrabajos pueden usar la terminal en un momento dado. De otro modo, habría multiples trabajostratando de leer de la terminal al mismo tiempo y se generaría mucha confusión acerca de cuál proceso

4A partir de ahora se usará esta palabra en vez de su análoga en castellano, por ser más intuitiva y conocida dentrodel marco de la computación.

Page 30: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 28

debe recibir cierta entrada ingresada por el usuario. Para prevenir esto, el intérprete debe cooperarcon el controlador de la terminal.

El intérprete puede darle acceso ilimitado a la terminal controladora a un solo grupo de procesospor vez. Este es llamado el foreground job (primer plano) en esa terminal. Otros grupos de procesos queestén ejecutando en la sesión sin dicho acceso a la terminal son los background jobs (segundo plano).

Un trabajo en segundo plano que intente leer de su tty es detenido automáticamente por el driver dela terminal. Más especí�camente, cuando un proceso llama a sys_read(), está llama a vfs_read(), quiena su vez invoca a �le->f_op->read(). Como se vio en la sección anterior, está función apunta a la rutinade lectura de nuestro driver tty: tty_read(), alocada en la estructura tty_fops (drivers/char/tty_io.c).tty_read() desreferencia la estructura tty_struct, almacenada en el campo private_data del inodoasociado al archivo especial al ser abierto por primera vez por chrdev_open(). Esta estructura contienetoda la información pertinente a la tty, como el tamaño de la ventana, el puntero a la estructura termios(encargada de almacenar todas las con�guraciones de línea para un puerto especí�co de un dispositivotty), el bu�er de lectura y el de escritura, semáforos (para evitar la concurrencia), registros de quiénesson el grupo y sesión que poseen la terminal, entre otras cosas más. Especialmente, también contieneun puntero a su disciplina de línea asociada. Recordemos que la disciplina de línea se encargaba demanejar el protocolo de la línea serial y, en nuestro análisis, será la encargada de manejar la lógica desi un proceso puede obtener datos desde la terminal o debe ser detenido. Cómo lo hace es muy simple.Una vez que tty_read() obtiene la tty_struct, está llama a la función de lectura de la disciplina delínea apuntada en esta estructura, denominada read_chan() y de�nida en drivers/char/n_tty.c. Estafunción primero chequea que no se requiera una lectura con un bu�er nulo y luego llama a job_control(),la cual devuelve error si:

La tty no tiene ningún grupo de procesos asociado.

Tenga grupo asociado, pero el proceso que requiere la operación sobre la terminal no es miembrode éste.

En este último caso, se emite una señal SIGTTIN la cual simboliza que un grupo background requiereacceso a la terminal y detiene a todos los procesos pertenecientes al grupo. Si el proceso ignora dichaseñal, un error EIO es retornado.

4.6.5. Formas de procesar la entrada

La forma canónica de procesar la entrada consiste en hacerlo en líneas completas terminadas conun retorno de carro, EOF o EOL. Ninguna entrada será retornada por el controlador de la tty hastaque toda la línea sea ingresada por el usuario. Por lo tanto, la funciones read retornan como máximola cantidad de bytes de la línea leída, sin importar si se requirió una entrada superior en un principio.En modo canónico el sistema operativo provee facilidades para editar la entrada. Algunos caracteresespeciales son usados para editar la línea a medida que el usuario la ingresa, por ejemplo, ERASE oKILL (asociados a las teclas <DEL>y Ctrl-u, respectivamente).

La forma no-canónica no provee a los usuarios de una forma de edición y todos los caracteres,exactamente como fueron ingresados, son pasados a la aplicación. Se deja relegada la responsabilidadde edición a la aplicación particular. La mayoría de los programas usan la terminal en modo canónicoy la única razón para no hacerlo es cuando éste acepta comandos de un solo carácter o cuando usa suspropias facilidades de edición.

La opción entre estos dos modos es controlada por la bandera ICANON en el campo c_l�ag de laestructura termios. Como ya mencionamos en la sección 4.6.4, esta estructura es usada para contenertodas las con�guraciones de línea para un puerto especí�co de la tty y se encuentra referenciada porun campo de la tty_struct. Entre algunas de las con�guraciones se encuentran:

La tasa de baudio5 actual.5Unidad que se usa para medir la velocidad de transferencia en las transmisiones telegrá�cas.

Page 31: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 29

El tamaño de los datos.

Con�guraciones en el �ujo de los datos.

4.6.6. El controlador de la consola

El driver de la tty maneja a dos dispositivos de la interfaz de usuario principal, la pantalla delsistema y el teclado. Estos dos juntos son denominados la consola. El código de la consola se encuentraen drivers/char. Cómo se inicializa el controlador del dispositivo y cómo realiza la lectura fue algo quevimos a lo largo de esta sección 4.6. Aquí nos enfocaremos en describir cómo imprime en la pantalla.

Cuando se escribe algo en la terminal, primero pasa por el procesamiento estándar de la tty, y luegoes llevado al controlador de la consola. Este controlador emula una terminal Linux (muy parecida auna terminal VT100) y analiza su entrada en busca de secuencias de escape de VT100 (movimientodel cursor, limpiar pantalla, etc). Los caracteres que no sean parte de la secuencia de escape sonconvertidos, en primera instancia, en Unicode, usando una de cuatro tablas de conversión (si la consolano estaba en modo UTF-8 en un principio), luego busca en la tabla la correspondencia entre valoresUnicode y la posición de la fuente, y escribe los índices de la fuente obtenidos (8 o 9 bits) en la memoriade video.

Hay varias consolas (llamadas Consolas Virtuales o Terminales Virtuales, abreviadas VC o VT, re-spectivamente), que comparten la misma pantalla. Uno puede usarlas independientemente, por ejemplo,para tener diferentes sesiones de login, o simplemente, para enviar alguna salida a éstas. Normalmente,el kernel aloja 6 terminales virtuales en cada inicio; init (ver sección 4.9.3.1) corre una sesión por cadaterminal pudiendo pasar de una a otra por medio de la secuencia de escape Alt-Fn.

La imagen en los monitores desaparecería casi instantáneamente a menos que no sea redibujadafrecuentemente por el controlador de video. Dado que el texto enviado a las terminales debe permanecer,la imagen de la pantalla es almacenada en memoria y debe ser escaneada (más o menos 60 veces porsegundo) para ser mantenida. Para el caso en que la terminal sea emulada utilizando dispositivos físicoscomo las placas de video, la memoria utilizada es de la propia placa.

La pantalla de una computadora puede ser manejada por un Adaptador Monocromático (MonochromeDisplay Adaptor, MDA), una tarjeta Hercules, un Adaptador de Grá�cos de Colores (Color GraphicsAdapter, CGA), un Adaptador de Grá�cos Mejorados (Enhaced Graphics Adapter, EGA) o un Ar-reglo de Grá�cos de Video (Video Graphics Adapter, VGA). Para el controlador de la consola estosdispositivos son vistos como un bloque de memoria de video en el cual pueden ser escritos caracterespara ser mostrados, un registro de E/S que asigna a el origen de la memoria de video el caracter queserá mostrado en la parte superior izquierda de la pantalla y otro registro que con�gura la posicióndel cursor del hardware. Cada caracter de la memoria ocupa dos bytes. El byte bajo es el código delcaracter y el alto es el �byte atributo�, o sea, un conjunto de bits que controla cómo el caracter seráimpreso (los colores del caracter mismo y su fondo para una tarjeta color o la intensidad/subrayadopara una tarjeta blanco y negro).

El origen de video puede ser usado o no, dependiendo si el desplazamiento (hacia arriba o abajo)por hardware (hard scrolling) este habilitado o no. En éste, el texto en la memoria de la terminalqueda tal y como está, y lo que cambia para saber qué mostrar en pantalla es el valor del registrodonde se almacena el origen de video. Este método es muy rápido y conveniente. La otro manera dehacer desplazamientos de pantalla, es a través de software (soft scrolling), el cual consiste en movertodo el texto de la pantalla hacia arriba o hacia abajo en la memoria de video. Esto es mucho máslento que el primer método.

Si usamos desplazamiento por hardware también cambiar entre terminales virtuales es muy sencillo.Cada consola virtuales posee una parte de la memoria de video del adaptador. El controlador devideo escoge que terminal mostrar, moviendo el origen de video. En cambio, realizar el cambio con eldesplazamiento por software requiere copiar el contenido de la pantalla de la consola anterior en lamemoria del kernel, y el contenido de la nueva consola (guardado en el kernel), es copiado a la memoria

Page 32: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 30

de video. Sólo la parte visible de la pantalla es guardada en el kernel, por lo que, cambiar de consolaimplica perder todo lo no visible y, por lo tanto, la información del desplazamiento.

Otra forma de manejar la pantalla es a través del dispositivo framebu�er, el cual provee unaabstracción para el hardware de grá�cos. Representa el bu�er de dispositivos de hardware de video ypermite a las aplicaciones acceder a este hardware a través de interfaces bien de�nidas. De esta manera,el software no tiene por qué saber sobre las cosas de bajo nivel del dispositivo.

4.7. Recorrido de rutas de archivos

Cuando un proceso debe actuar sobre un archivo, pasa la ruta de éste a alguna llamada al sistemadel VFS, como open(), mkdir(), o stat(). En esta sección, se mostrará cómo el VFS realiza el pathnamelookup (recorrido del camino), o sea, cómo deriva un inodo a partir de una ruta de archivo dada.

El procedimiento estándar para realizar esta tarea consiste en analizar la ruta y descomponerla enuna secuencia de nombres de archivos. Todos estos nombres, salvo el último, deben ser interpretadoscomo directorios.

Si el primer carácter del pathname es /, el pathname es absoluto, y la búsqueda comienza a partirdel directorio identi�cado por current->fs->root (el directorio root del proceso). Caso contrario, elpathname es relativo, y la búsqueda empieza desde el directorio identi�cado por current->fs->pwd (eldirectorio actual del proceso).

Teniendo la estructura dentry y, por lo tanto, el inodo, del directorio inicial, el código examina laentrada que corresponda al primer nombre para derivar el inodo correspondiente. Luego, el archivo odirectorio que tenga ese inodo es leído desde el disco y la entrada que corresponda al segundo nombrees analizada para obtener su inodo. Este método es repetido por cada nombre incluido en la ruta.

La dentry cache acelera el proceso considerablemente porque mantiene a los objetos dentry másrecientemente usados en memoria evitando una o más lecturas del disco durante el recorrido.

Sin embargo, las cosas no son tan sencillas ya que algunas características de Unix y el VFS debentomarse en cuenta:

Los derechos de acceso de cada directorio deben ser chequeados para veri�car si el proceso tienepermisos para leer el contenido del directorio o no.

Un nombre de archivo puede pertenecer a un link simbólico que corresponde a una ruta arbitraria;en este caso, el análisis debe ser expandido a todos los componentes de la ruta.

Los links simbólicos pueden inducir referencias circulares; el kernel debe estar atento a estaposibilidad y romper ciclos in�nitos cuando estos ocurran.

Un nombre de archivo puede ser el punto de montaje de un sistema de archivos montado. Estasituación debe ser detectada, y la búsqueda debe continuar dentro del nuevo sistema de archivos.

De todas estas características, la que más nos interesa es la primera. Si pudiésemos agregar alos chequeos estándares del kernel, nuestra comprobación entre clases de acceso, lograríamos nuestroobjetivo de forma prolija y transparente.

La función encargada del recorrido del camino, denominada path_lookup(), se encuentra en fs/-namei.c y recibe tres argumentos:

Un puntero a la cadena que contiene el pathname a ser resuelto.

Diferentes banderas que representan cómo el archivo a buscar será accedido. Por ejemplo, LOOKUP_PARENT,que requiere que se busque hasta el directorio del último componente de la ruta, o LOOKUP_CREATEque informa que lo que se intenta es crear el archivo correspondiente al nombre del último com-ponente de la ruta.

Page 33: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 31

Un puntero a una estructura nameidata, de�nida en include/linux/namei.h, la cual almacenarálos resultados de la operación de lookup. Entre sus campos se encuentran el dentry del archivoencontrado, un puntero a la estructura vfs_mount del sistema de archivos donde se encontró, lasbanderas mencionadas en el item anterior, la profundidad de la búsqueda (útil para romper losbucles in�nitos), etc.

path_lookup() llama a link_path_walk(), la cual es el núcleo de la operación para recorrer el camino.link_path_walk() realiza el procedimiento que describimos anteriormente, fragmentando la ruta endiferentes nombres y analizando uno por uno hasta tener todo el camino resuelto. Una vez que obtuvoun inodo, o sea, que resolvió un nombre dado, comprueba que no es el último, y de ser así, se preparapara analizar el inodo obtenido para poder resolver el siguiente nombre. Antes de hacer esto, se encargade llamar a la función exec_permission_elite() o en caso de que ésta falle, a vfs_permission(). Estasfunciones tienen como objetivo implementar el chequeo para comprobar si el nuevo directorio puede seraccedido, retornando un error en caso de que ésto no sea posible. En verdad, exec_permission_elite()falla si el archivo tiene de�nida una vfs_permission(), por lo que solo una de las dos se ejecutará. Decualquier modo, ambas rutinas invocan al hook security_inode_permission() del LSM justo antes deretornar.

Una vez que se detecta que se debe resolver el último componente de la ruta link_path_walk()procede de la siguiente forma:

Si la bandera LOOKUP_PARENT está activada. Esta bandera se activa cuando se necesitabuscar hasta el penúltimo componente de la ruta, ya que el objetivo es realmente éste. Porejemplo cuando se debe crear un archivo.

Si la bandera LOOKUP_FOLLOW está activada, lo que signi�ca que el último componente esun link simbólico y éste debe ser resuelto.

Si cualquiera de las banderas anteriores está limpia, se obtiene el inodo correspondiente al últimocomponente.

4.8. Gestión de archivos de dispositivos

El mecanismo de gestión de dispositivos actual de Linux se conforma por la integración de concep-tos o tecnologías: UDEV, SYSFS, Device Driver Model, KOBJETOS. En las siguientes secciones sedetallará cada uno de estos tópicos; para más referencias consultar [12, 13, 14, 15].

4.8.1. Problemas de la gestión tradicional de dispositivos

Los archivos o nodos de dispositivos han sido usados desde versiones tempranas de Unix. Un archivode dispositivo es, usualmente, un archivo real en un sistema de archivos dado. Su inodo, sin embargo,no necesita incluir punteros a los bloques de información en disco (los datos del archivo), ya que nohay ninguna. En cambio, el inodo debe incluir un identi�cador del dispositivo de hardware al cualrepresenta.

El identi�cador consiste en el tipo de archivo de dispositivo (carácter o bloque) y un par de números.El primer número, denominado número mayor (mayor number), identi�ca el tipo de dispositivo. Tradi-cionalmente, todos los archivos que tengan el mismo número mayor y el mismo tipo comparten el mis-mo conjunto de operaciones de archivos, porque son manejados por el mismo controlador. El segundonúmero, llamado número menor (minor number), identi�ca el dispositivo especí�co entre el grupo deaquellos que comparten el mismo número mayor. Por ejemplo, un grupo de discos manejados por elmismo controlador de discos, tienen el mismo mayor, pero diferente menor. Notar que los dispositivosde carácter y de bloque tienen una numeración independiente, por ende, el dispositivo de bloque (3,0)es diferente al de carácter (3,0).

Page 34: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 32

La llamada al sistema sys_mknod() es invocada para crear archivos de dispositivos. Recibe elnombre de éste, su tipo, y los números mayor y menos como sus parámetros. Los nodos de dispositivosson, comúnmente, incluidos en el directorio /dev.

Ahora repasemos un poco de historia. Tradicionalmente, los sistemas Linux en general utilizabanun método estático de creación de dispositivos, implicando que un gran número de nodos de dispositivoeran creados en /dev (literalmente, cientos de nodos) sin tener en cuenta si el hardware correspondienteexistía en realidad. Esto se hacía, típicamente, mediante un guión de arranque, MAKEDEV, quecontenía una serie de llamadas al programa mknod con los números mayor y menor correspondientes acada posible dispositivo que pudiera existir en el mundo. Además, la forma de acceder a un periféricoconcreto no es siempre la misma, ya que depende de qué otros aparatos hay conectados: si se conectanlos discos A y B, se llamarán disco1 y disco2 respectivamente. Pero si está sólo un disco (el B, porejemplo), se llamará disco1, porque sólo hay uno. El B ha cambiado de nombre. Este modelo de gestiónde dispositivos daba algunos problemas:

El directorio /dev era enorme y difícil de manejar, ya que incluía a todos los dispositivos posibles.

Los números mayor y menor que se asociaban a cada dispositivo se estaban acabando.

Los usuarios necesitaban que cada dispositivo sea accesible de la misma manera; no aceptabanque por conectar un disco USB al sistema tengan que recon�gurar la cámara de vídeo.

Los programas necesitaban poder detectar cuándo se había conectado o desconectado un dispos-itivo, y cuál era la entrada que se le había asociado en /dev.

En Febrero de 2000, un nuevo sistema de �cheros llamado devfs fue incluido en los núcleos 2.3.46 yestuvo disponible en la serie 2.4 de los núcleos estables. Aunque estaba presente en las propias fuentesdel núcleo, este método de creación dinámica de dispositivos nunca recibió mucho apoyo por parte delequipo de desarrolladores del kernel. A pesar de solucionar algunos de los problemas originales, no lohacía con todos.

El principal problema con el sistema adoptado por devfs era el modo en el que manejaba la de-tección, creación y denominación de dispositivos. El último punto, la denominación de los nodos, fuequizás el más crítico. Está generalmente aceptado que si los nombres de dispositivos son con�gurables,entonces las políticas de denominación deberían ser establecidas por un administrador del sistema y noimpuestas por un desarrollador en particular. El sistema de �cheros devfs sufría también de extrañoscomportamientos inherentes a su diseño y que no podían corregirse sin una revisión sustancial delnúcleo. Durante un tiempo fue marcado como descartado debido a la falta de mantenimiento, siendoremovido �nalmente del núcleo en Junio de 2006.

4.8.2. Device Driver Model

Una nueva característica signi�cativa en el kernel 2.6 es la adición de un modelo de dispositivosuni�cado, el Device Driver Model. Este modelo provee un solo mecanismo para representar dispositivosy describir su topología en el sistema. Tal mecanismo conlleva varios bene�cios:

Minimización de la duplicación de código.

Un mecanismo para proveer facilidades comunes, como un conteo de referencias (reference count-ing).

La posibilidad de enumerar a todos los dispositivos del sistema, observar su status y ver a québus están ligados.

La posibilidad de generar un árbol válido y completo de toda la estructura de dispositivos en elsistema, incluyendo buses e interconexiones.

Page 35: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 33

La posibilidad de enlazar a los dispositivos con sus respectivos controladores y vice versa.

La posibilidad de categorizar dispositivos por su clase, como un dispositivo de entrada, sin lanecesidad de tener que entender la topología física del dispositivo.

La habilidad de soportar la inserción y eliminación de dispositivos mientras el sistema está cor-riendo.

La habilidad de alojar transparentemente recursos al con�gurar un dispositivo.

La habilidad de recorrer el árbol de dispositivos desde sus hojas hasta la raíz, apagando a losdispositivos en orden correcto.

Esta fue la motivación inicial del device driver model. Para implementar inteligentemente la admin-istración de la energía (power management), uno tiene que poder construir un árbol representando latopología de los dispositivos en el sistema. Cuando se apaga un dispositivo en un árbol que crece haciaabajo, el kernel debe apagar los nodos inferiores (hojas) antes que los superiores. Por ejemplo, el kerneldebe apagar un mouse USB antes que el controlador USB, y el kernel debe apagar el controlador USBantes que el bus PCI. Para realizar esta tarea precisa y e�cientemente, el kernel necesita un árbol dedispositivos.

Para lograr todos estos objetivos se necesita un mecanismo e�ciente para comunicar datos entreel espacio de usuario y el kernel y viceversa. Con el desarrollo del árbol inestable 2.5 del núcleo,posteriormente liberado como núcleos estables de la serie 2.6, aparece un nuevo sistema de �cherosvirtual llamado sysfs. El trabajo de sysfs es exportar una visión de la con�guración hardware delsistema a los procesos de usuario. Con esta representación visible a nivel de usuario, la posibilidadde encontrar un sustituto para devfs a nivel de usuario se hace mucho más real. Sysfs es entonces,un mecanismo conceptualmente elegante y e�ciente para que el núcleo comunique datos al espacio deusuario. Más aun, sysfs permite no solo que el usuario lea datos sobre la con�guración de un driveren particular, sino también modi�car esos datos, creando una asociación uno a uno entre archivosen espacio de usuario con estructuras internas del kernel. El lado del núcleo de sysfs se implementamediante kobjetos (ver sección siguiente), mientras que el lado de espacio de usuario esta a cargo delsistema Udev (ver sección 4.8.5).

4.8.3. Kobjeto

En el corazón del driver device model está el kobjeto, el cual es representado por la estructurakobject, de�nida en include/linux/kobject.h. El kobjeto es similar a la clase objeto de los lenguajesbasados en objetos como C# o Java. Provee utilidades básicas, como conteo de referencia6, un nombre,y un puntero a un padre, permitiendo la creación de una jerarquía de kobjetos.

struct kobject {

char *k_name;

char name[KOBJ_NAME_LEN];

struct kref kref;

struct list_head entry;

struct kobject *parent;

struct kset *kset;

struct kobj_type *ktype;

struct dentry *dentry;

}

6Simboliza cuantas entidades dentro del kernel están usando un objeto dado. Cuando la referencia llegue a cero,signi�ca que nadie más lo utiliza y su memoria puede ser liberada.

Page 36: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 34

Parte de la di�cultad en entender el driver model (y la abstracción de objetos en la que se construye)es que no hay un punto de partida obvio. Tratar con kobjetos requiere entender un grupo de diferentestipos, todos haciendo referencias entre ellos. En un intento por hacer las cosas más fáciles, se empezarádando de�niciones algo vagas, para luego ir puliéndolas con más detalles más adelante.

La infraestructura del kobjeto realiza una administración de objetos básica que estructuras de datosmás grandes y subsistemas pueden utilizar. Entre las funcionalidades primarias se encuentran:

Conteo de referencia del objeto (nombrada más arriba).

Mantenimiento de lista (conjuntos) de objetos.

Control de concurrencia (locking) de conjuntos de objetos.

Representación en espacio de usuario (en sysfs).

La infraestructura consiste en un número de tipos de objetos que soportan dicha funcionalidad. Susinterfaces de programación serán descritas resumidamente más adelante. Los tipos son:

kobjects: generalmente no son interesantes por si solos; en cambio, normalmente están embebidosdentro de otra estructura que contiene el código que realmente interesa.

ktype : está asociado a cada kobjeto. Controla qué sucede cuando un kobjeto ya no es referenciadoy la representación por defecto en el sysfs.

kset : es un grupo de kobjetos todos embebidos en estructuras del mismo tipo. El kset es elcontenedor básico para colecciones de kobjetos. Los ksets contienen a su propio kobjeto, lo quesigni�ca que el padre de un kobjeto es, usualmente, el kset que lo contiene.

subsystem : es una colección de ksets que, colectivamente, constituyen una sub-parte mayor delkernel. Los subsistemas normalmente se corresponden con los directorios de nivel más alto en elsysfs.

Los kobjetos mantienen una relación muy cercana con el sistema de archivos sysfs. Cada kobjetoque se registra recibe un directorio en el sysfs. Atributos acerca de los kobjetos pueden luego serexportados.

4.8.3.1. Kobjetos embebidos

Es raro (hasta desconocido) que el código del kernel cree kobjetos por sí solos; en cambio, loskobjetos son usados para controlar el acceso a un objecto más grande, de un dominio especí�co. Paraeste �n, se encontrará a los kobjetos embebidos en otras estructuras. Estos pueden ser vistos (entérminos de orientación a objetos) como la clase abstracta a partir de la cual las otras clases derivan.Un kobjeto implementa una serie de habilidades que no son útiles por sí solas, pero que son muybene�ciosas para otras estructuras. Por ejemplo la struct cdev describe un dispositivo de carácterdentro del kernel:

struct cdev {

struct kobject kobj;

struct module *owner;

struct file_operations *ops;

struct list_head list;

dev_t dev; /* Números mayor y menor */

}

Page 37: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 35

El código que funciona con kobjetos a veces tendrá el problema de necesitar una referencia a laestructura que contiene a éste. La siguiente función de�nida en include/linux/kernel.h soluciona elproblema:

container_of(pointer, type, member)

supongamos que tenemos un puntero a un kobjeto, *kp, de una estructura cdev llamada tty :

container_of(kp, struct cdev, kobj)

nos devuelve (struct cdev *) tty.

4.8.3.2. Inicialización de kobjetos

Obligatoriamente para usar un kobjeto hay que inicializarlo con la función kobject_init() enlib/kobject.c. Entre otras cosas, esta función asigna 1 al conteo de referencia del kobjeto. Además,los usuarios de un kobjecto deben asignarle como mínimo un nombre, que será el nombre que seutilizará para las entradas en el sysfs. Se puede copiar directamente una string (previa alojamientode memoria) en el campo name, pero es un enfoque que debería ser evitado. En cambio, la funciónkobject_set_name() hace mejor el trabajo.

Los otros campos que deberían ser iniciados, directa o indirectamente, por su creador son ktype,kset y su padre.

4.8.3.3. Conteo de referencias

Una de la funciones principales del kobjeto es servir como un conteo de referencias para la estructuraen el que está embebido. Mientras el conteo de referencias no sea 0, la estructura debe existir. Las dosfunciones de bajo nivel para manipular el conteo de referencia son:

struct kobject *kobject_get (struct kobject *kobj);

void kobject_put (struct kobject *kobj);

Una llamada exitosa a kobject_get() incrementará el conteo y retornará el puntero al kobjeto. Si elkobjeto está en proceso de destrucción, la operación fallará, retornando NULL.

Cuando una referencia es liberada, kobject_put() decrementará el conteo, y posiblemente liberaráal objeto. Notar que kobject_init() inicializaba el conteo en 1, por ende, el código que utiliza el kobjetotendrá que, eventualmente, llamar a kobject_put() para liberar esa referencia.

4.8.3.4. Registrándose en el sysfs

Un kobjeto inicializado no aparecerá en el sysfs a menos que el kernel cree las entradas sysfs através de la función:

int kobject_add (struct kobject *kobj);

En tanto que la función:

void kobject_del (struct kobject *kobj);

lo removerá.La función kobject_register() que es una combinación de las llamadas a kobject_init() y kob-

ject_add(). De manera similar, kobject_unregister() llamará a kobject_del() y luego a kobject_putpara liberar la referencia inicial creada por kobject_init().

Page 38: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 36

4.8.3.5. Ktypes y métodos release

Un aspecto importante que es qué sucede con el kobjeto cuando su conteo de referencia llega acero. El código que lo creó, generalmente, no sabe cuándo esto va a ocurrir. Una estructura protegidapor un kobjeto no puede ser liberada antes de que el conteo de referencia llegue a cero. Entonces elcódigo debe, asíncronamente, ser noti�cado cuando la última referencia a un kobjeto se haya eliminado.La noti�cación se realiza a través del método release() del kobjeto. Usualmente, este método tiene laforma de:

void my_object_release (struct kobject *kobj)

{

struct my_object *mine = container_of(kobj, struct my_object, kobj);

/* Realizar cualquier tipo de limpieza adicional en el objeto,

* y luego ...

*/

kfree(mine);

}

Todo kobjeto debe tener un método release() y el kobjeto debe persistir en forma consistente hastaque ese método sea invocado. Si esto no se cumple, el código es defectuoso.

Lo interesante aquí es que release() no está almacenado en el kobjeto en sí, sino que está asociadocon su ktype. Entonces, introduzcamos la estructura kobj_type:

struct kobj_type {

void (*release)(struct kobject *);

struct sysfs_ops *sysfs_ops;

struct attribute **default_attrs;

}

El �n de la estructura es describir un tipo particular de kobjeto (o más correcto, del objeto que locontiene). Cada kobjeto necesita tener una estructura kobj_type asociada; un puntero a dicha estructurapuede ser ubicado en el campo kobj_type en la etapa de inicialización o (lo más común) puede serde�nida por su kset contenedor. La estructura kobj_type es descripta en la sección 4.8.4.2.

El campo release de kobj_type es, obviamente, un puntero al método release() para este tipo dekobjeto. Los otros dos campos (sysfs_ops y default_attrs) controlan cómo los kobjetos de este tiposon representados en el sysfs, y serán tratados en secciones posteriores.

4.8.3.6. Ksets

Un kset es como una extensión del kobj_type, pues un kset es una colección de kobjetos idénticos.Pero, mientras kobj_type se preocupa acerca del tipo de kobjeto, el kset se preocupa por la agregacióny la colección. Los dos conceptos han sido separados para que kobjetos de tipos idénticos puedanpertenecer a conjuntos diferentes.

Un kset:

struct kset_uevent_ops {

int (*filter)(struct kset *kset, struct kobject *kobj);

const char *(*name)(struct kset *kset, struct kobject *kobj);

int (*uevent)(struct kset *kset, struct kobject *kobj, char **envp,

int num_envp, char *buffer, int buffer_size);

};

Page 39: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 37

struct kset {

struct subsystem * subsys;

struct kobj_type * ktype;

struct list_head list;

spinlock_t list_lock;

struct kobject kobj;

struct kset_uevent_ops * uevent_ops;

};

sirve para:

Ser una bolsa contenedora de un grupo de objetos idénticos. Por ejemplo, puede ser usado porel kernel para rastrear a todos los dispositivos de bloque o todos los drivers PCI.

Ser el instrumento que le da consistencia al device driver model (y sysfs). Cada kset contiene unkobjeto que puede ser con�gurado para que sea el padre de otros kobjetos. Esta es la forma deconstruir la jerarquía del modelo de dispositivos.

Soportar el �hotplugging� (conectar en caliente) de los kobjetos e in�uenciar cómo los eventos dehotplug son reportados al espacio de usuario. Estos eventos son los que utiliza Udev para crearlos nodos de dispositivos.

En términos de orientación a objetos, los ksets heredan también su propio kobjeto, por lo quepueden ser tratados como uno de éstos también. Un kset contiene sus hijos en una lista enlazada ysus hijos apuntan nuevamente a éste a través del campo kset y, en la mayoría de los casos, del campoparent.

Para inicialización y con�guración, los ksets tienen una interfaz muy similar a la de los kobjetos:

int kset_init (struct kset *kset);

int kset_add (struct kset *kset);

int kset_register (struct kset *kset);

void kset_unregister(struct kset *kset);

Los ksets tienen también un nombre y un tipo. El primero se de�ne asignándole el nombre dekobjeto incluido en el kset. El segundo se asigna a través del campo ktype y luego de ésto todos loskobjetos contenidos en el kset serán de este nuevo tipo.

Otra característica de los ksets es un conjunto de operaciones para soportar hotplugging ; estasoperaciones son invocadas cada vez que un kobjeto entra o sale de un kset. Tienen la habilidad dedeterminar cuándo generar un evento de espacio de usuario para este cambio, y afectar el modo en queeste evento es generado. Las dos formas más comunes son:

Comunicar el evento a través de un socket con el protocolo NETLINK para comunicar informaciónentre el kernel y el espacio de usuario. Es el método que más se utiliza. De hecho, el demonioudevd crea un socket con este protocolo y escucha constantemente para saber cuando crear oborrar un nodo de dispositivo.

Comunicar el evento a través de un programa de usuario que el kernel ejecuta con la funcióncall_usermodehelper() de�nida en include/linux/kmod.h. La información de qué tipo de eventosucedió, el nombre del dispositivo, entre otras cosas, son pasados al programa a través de variablesde entorno situadas en el tercer argumento de main(), envp.

Uno se puede preguntar cómo, exactamente, un kobjeto es agregado a un kset si no hay ningunafunción para realizar ésto. La respuesta es que kobject_add() realiza la tarea, siempre y cuando, elkobjeto pasado a esta rutina tenga su campo kset inicializado con el kset al que pertenece.

Para �nalizar, el kset contiene un campo que apunta a su subsistema, llamado subsys. Entonces,es tiempo de hablar de éstos.

Page 40: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 38

4.8.3.7. Subsistemas

Un subsistema es la representación de una porción de alto nivel del kernel como un todo. Es unaestructura simple:

struct subsystem {

struct kset kset;

struct rw_semaphore rwsem;

};

Un subsistema, por lo tanto, es simplemente un envoltorio alrededor de un kset. De hecho, esto noes tan simple, ya que un solo subsistema puede incluir múltiples ksets, pero como hay un solo campokset, no se podrá encontrar directamente a todos éstos. El semáforo de lectura-escritura rwsem servirápara serializar el acceso a la lista interna de cada kset.

Los subsistemas son, normalmente, declarados con la macro especial:

decl_subsys (char *name, struct kobj_type *type,

struct kset_hotplug_ops *hotplug_ops);

La interfaz del sistema de archivos está dada por:

void subsystem_init (struct subsystem *subsys);

int subsystem_register (struct subsystem *subsys);

void subsystem_unregister (struct subsystem *subsys);

struct subsystem *subsys_get (struct subsystem *subsys)

void subsys_put (struct subsystem *subsys);

La mayoría de estas funciones actúan sobre el kset embebido.

4.8.4. Sysfs

El sistema de archivos sysfs es un sistema de archivos virtual en memoria que provee una vistajerárquica de los kobjetos. Le permite a los usuarios ver la topología de los dispositivos en su sistemacomo un simple sistema de archivos. Los kobjetos pueden exportar archivos que permiten leer y,opcionalmente, escribir variables del kernel, usando atributos.

Aunque el propósito inicial del modelo de dispositivos era proveer una topología de dispositivospara la administración de la energía (power management), sysfs se transformó en su espectacularconsecuencia. Para facilitar el análisis de errores del código, el desarrollador del device driver modeldecidió exportar el árbol como un sistema de archivos. Esto rápidamente probó ser muy útil comouna vista poderosa hacia la jerarquía de objetos del sistema. De hecho, sysfs, originalmente llamadodriverfs, carecía de los kobjetos. Eventualmente, sysfs mostró claramente que un nuevo modelo deobjetos sería muy bene�cioso, y el kobjeto nació. Hoy en día todo sistema con un kernel 2.6 tiene sysfsy la mayoría lo tiene montado.

La magia detrás del sysfs es ligar kobjetos con directorios a través del campo dentry dentro de todokobjeto. Recordemos que los objetos dentry representan entradas de directorio y, por ende, gracias aesta asociación se puede mapear cada kobjeto con un directorio. Exportar el kobjeto como un sistemade archivos es ahora tan fácil como construir un árbol de estructuras dentry en memoria. Pero, lainfraestructura de kobjetos ya forma un árbol: el device driver model. Entonces, con los kobjetosasociados a dentries y la jerarquía de objetos formando un árbol en memoria, la implementación delsysfs se transforma en trivial. Los directorios de más alto nivel en este sistema de archivos son:

block

Los dispositivos de bloque, independientemente del bus al que están conectados.

Page 41: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 39

devices

Todos los dispositivos de hardware reconocidos por el kernel, organizados de acuerdo al bus alque están conectados.

bus

Los buses del sistema, an�triones de los dispositivos.

drivers

Los controladores de dispositivos registrados en el kernel.

class

Los tipos de los dispositivos en el sistema (tarjetas de audio, placas de red, tarjetas grá�cas,etc); la misma clase puede incluir dispositivos contenidos por diferentes buses y controlados pordiferentes drivers.

power

Archivos para manejar los estados en la potencia de algunos dispositivos de hardware.

�rmware

Archivos para manejar el �rmware.

Las relaciones entre componentes del device driver model son expresadas como enlaces simbólicosentre directorios y archivos. Por ejemplo, el archivo /sys/block/sda/device puede ser un link simbólico deun subdirectorio incluido en /sys/devices/pci0000:00 representando a la controladora SCSI conectadaal bus PCI. Más aún, sys/block/sda/device/block es un enlace simbólico a sys/block/sda, estableciendoque este dispositivo PCI es el controlador del disco SCSI.

El propósito de los archivos regulares en sysfs es representar atributos de los controladores ydispositivos. Por ejemplo, el archivo dev en /sys/block/hda contiene los números mayor y menor deldisco maestro del primer IDE.

Lo último que queda en nuestro análisis del sysfs y los kobjetos es la interfaz que une a ambos.

4.8.4.1. Obtener entradas de sysfs

Como vimos en la sección de los kobjetos, las dos funciones para registrar un kobjeto en el sys-fs son: kobject_add() y kobject_register(). La primera, necesita que el kobjeto sea inicializado conkobject_init(), mientras que la segunda es una conjunción de las dos.

El nombre del directorio será el mismo que el nombre del kobjeto en cuestión y su localizaciónre�ejará la posición del kobjeto en la jerarquía que uno de�nió.

Vale aclarar que ninguna de estas funciones inicializa los campos kset, parent, ktype ni name delkobject. El programador debe asignar valores a estos campos dependiendo de dónde quiere situar elkobjeto en la jerarquía.

4.8.4.2. Habitando el directorio del kobjeto

Ubicar un kobjeto en la jerarquía del sysfs es solo el primer paso para gestionar el dispositivoque este representa. Por lo general, un dispositivo posee numerosos parámetros que determinan sufuncionamiento y características técnicas. Cada uno de estos parámetros se representa en el kobjeto (yen consecuencia en el sysfs) como una instancia de tipo attribute:

Page 42: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 40

struct attribute {

char *name;

struct module *owner;

mode_t mode;

};

Esta estructura forma parte del tipo kobj_type (ver sección 4.8.3.5) el cual, a su vez, es parte de lade�nición de kobject (campo ktype). default_attrs es el campo de kobj_type que apunta a un arreglo depunteros a estructuras attribute, el cual, como su nombre sugiere, representa el conjunto de atributospor defecto que ese tipo de kobjeto debe tener.

En la estructura attribute, name es el nombre del atributo (como aparecerá en sysfs), owner es unpuntero al módulo (si hay uno) responsable de la implementación del atributo, y mode son los bitsde protección que deben ser aplicados al atributo. El modo es usualmente S_IRUGO para atributosde solo lectura o S_IWUSR para dar acceso de escritura solo a root. La última entrada del arreglodefault_attrs debe ser NULL.

El arreglo default_attr dice cuáles son los atributos, pero no le dice al sysfs cómo implementarlos.Esa tarea recae en el campo sysfs_ops de kobj_type, que apunta a una estructura de�nida así:

struct sysfs_ops {

ssize_t (*show)(struct kobject *kobj, struct attribute *attr,

char *buffer);

ssize_t (*store)(struct kobject *kobj, struct attribute *attr,

const char *buffer, size_t size);

};

Estas funciones serán llamadas por cada operación de lectura y escritura, respectivamente, sobreun atributo de un kobjeto de cierto tipo. En cada caso, kobj es el kobjeto cuyo atributo está siendoaccedido, attr es la estructura attribute del atributo en cuestión y bu�er es un bu�er de una páginade memoria para los datos del atributo.

La función show() debe guardar el valor completo del atributo en el parámetro bu�er, asegurándosede no sobrepasar PAGE_SIZE bytes. Por convención, los atributos deben contener valores simples o,como máximo, un arreglo de valores similares, por lo que el límite de una página nunca debería serun problema. El valor de retorno es, por supuesto, la cantidad de caracteres escritos o leídos, segúncorresponda.

La función store() tiene un interfaz parecida; el parámetro adicional size da la longitud de losdatos recibidos desde espacio de usuario. Nunca olvidar que bu�er contiene información provista desdeespacio de usuario y no chequeada, por lo que debería ser cuidadosamente tratada y asegurarse de queencaja en el formato que se requiere.

4.8.4.3. Atributos adicionales

En muchos casos, el campo default_attrs describe a todos los atributos que un kobjeto tendrásiempre. Sin embargo, a veces es posible que sean necesarios más, por lo que se permite agregar (yquitar) nuevos atributos a través de las siguientes funciones:

int sysfs_create_file (struct kobject *kobj, struct attribute *attr);

int sysfs_remove_file (struct kobject *kobj, struct attribute *attr);

Notar que las funciones show() y store() serán invocadas para implementar las operaciones en losnuevos atributos. Antes de añadir uno a un kobjeto, uno debería realizar todos los pasos necesariospara asegurarse que dichas funciones podrán saber cómo implementar el atributo.

Page 43: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 41

4.8.4.4. Links simbólicos

Por último, concisamente, las dos funciones que proveen la interfaz para crear link simbólicos entremiembros del sysfs son:

int sysfs_create_link (struct kobject *kobj, struct kobject *target);

char *name);

int sysfs_remove_link (struct kobject *kobj, char *name);

La primera función creará un link (llamado name) que apunte a una entrada del sysfs que cor-responda al kobjeto target, como un atributo de kobj. El link será persistente aún cuando target searemovido del sistema.

4.8.5. Udev

Udev es un conjunto de procesos de usuario encargado de gestionar los dispositivos tomando lainformación provista por el núcleo a través de sysfs. Udev sólo creará los nodos correspondientes aaquellos dispositivos detectados por el núcleo. Debido a que estos nodos de dispositivo se crearán cadavez que se inicie el sistema, se almacenarán en un sistema de �cheros tmpfs (el cual existe por completoen memoria). Los nodos de dispositivo no necesitan mucho espacio, por lo que la memoria utilizada esmuy poca.

Udev resuelve los problemas anteriores de la siguiente forma:

Sólo dispositivos conectados

Udev mantiene en /dev sólo las entradas correspondientes a los dispositivos que hay conectadosal sistema. Así se soluciona el problema del /dev super poblado.

No se usa mayor y menor

No se usa los números mayor y menor para reconocer a cada dispositivo. El dispositivo puedefuncionar incluso aunque estos números estén elegidos al azar. Por tanto, no le afecta el que seacaben las combinaciones mayor/menor asignables.

Permite dar nombre �jo

Permite dar un nombre �jo para cada dispositivo, por ejemplo cámara, sin que éste dependa dequé otros dispositivos haya conectados ni del orden en que se hayan conectado. Un disco duro,por ejemplo, se reconoce por el identi�cador de su sistema de �cheros, el nombre del disco, y elconector físico en el que está.

Avisa a los programas

Avisa mediante mensajes D-BUS a los programas del espacio de usuario cuando un dispositivose conecta o desconecta. También permite a los programas consultar la lista de dispositivosconectados y la forma de acceder a cada uno.

Todo en espacio de usuario

Udev hace que toda la política de nombres de dispositivo esté en espacio de usuario, y no en elkernel. Esto hace posible que un programa cualquiera pueda decidir el nombre de un dispositivo,por ejemplo basándose en sus características. No es necesario modi�car el kernel si no se estáconforme el nombre que se le da a un dispositivo.

Udev se ejecuta mediante el demonio: el proceso udevd que detecta cuándo se ha conectado odesconectado un dispositivo del sistema. Cuando ocurre uno de estos eventos, Udev obtiene informacióndel contexto (subsistema del kernel, conector físico usado, nombre dado por el kernel, etc) y tambiéndel propio dispositivo (número de serie, fabricante, etc). Esta información la puede encontrar mediantelos datos que hay en /sys, donde está montado sysfs.

Page 44: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 42

4.9. Identidad de procesos y usuarios y el mecanismo de autenticación

4.9.1. La Persona de un proceso

En todo momento, cada proceso tiene un ID de usuario efectivo (e�ective user ID o EUID), unID efectivo de grupo (e�ective group ID o EGID) y un conjunto de IDs suplementarios. Estos IDsdeterminan el privilegio de un proceso. Son colectivamente llamados la persona o credenciales delproceso y determinan �quién es� para propósitos de control de acceso.

El interprete de comandos para el usuario es iniciado con la persona que consiste en el UID delusuario ingresante y su GID. Normalmente, todos los demás procesos heredan estos valores.

Un proceso también posee un real user ID que identi�ca al usuario que creo el proceso y, también,un real group ID. Estos valores no toman parte del control de acceso.

Los valores UID y GID pueden ser cambiados y la situación más obvia donde ocurre esto es en elprograma login. Cuando éste es ejecutado, su UID es root, pero debe invocar un interprete de comandospara que el usuario pueda desarrollar tareas dentro del sistema. Este shell debe ser iniciado con el UIDy GID del usuario, ya que, de no hacerlo, el usuario recientemente validado tendrá los privilegios pararealizar lo que quiera. Por lo tanto es sistema operativo ofrece un mecanismo llamado setuid, para quedeterminados usuarios y/o programas puedan cambiar el UID de ciertos procesos.

El caso más común para cambiar la persona es cuando un programa ordinario necesita acceso aun recurso que, en condiciones normales, nunca sería accesible por el usuario corriendo el programa.Sin embargo, permitir esto puede ser una fuente de violaciones no intencionales de privacidad o abusointencional. Por lo tanto, cambiar la persona de un proceso está restringido a situaciones especiales.

Uno no puede arbitrariamente cambiar el UID o GID a cualquier cosa que quiera; solo procesosprivilegiados pueden realizarlo. En cambio, la forma normal para que un programa cambie su personaes que haya sido con�gurado anteriormente para poder cambiar a un usuario o grupo particular. Estaes la función de los bits setuid y setgid en el modo de acceso de un archivo.

Cuando el bit setuid en un archivo ejecutable está activado, ejecutar tal archivo otorga al procesoun tercer ID de usuario: el �le user ID. Este es con�gurado con el ID del dueño del archivo. El sistemacambia el UID efectivo por el �le UID. Si un proceso tiene un �le ID puede cambiar su ID efectivo a suID real y, de vuelta, a su �le ID. Los programas usan este recurso para dejar sus privilegios especialesexcepto cuando realmente los necesitan.

Un ejemplo es el programa para cambiar la contraseña de usuario, /usr/bin/passwd. Todos lascontraseñas son almacenadas en un archivo común, pero este archivo no puede ser editado directamenteporque está protegido. Entonces, el usuario invoca el programa passwd que tiene el bit setuid activadoy cuyo dueño es root. Cuando el proceso �forkeado� por el interprete ejecuta tal programa, los camposeuid y fsuid en su task_struct son iniciados en 0. Ahora el proceso puede acceder al archivo porque,cuando el kernel realice el control de acceso, encontrará un valor 0 en el fsuid. Por supuesto, el programapasswd no le permite al usuario hacer otra cosa que no sea cambiar la contraseña.

Un proceso puede cambiar sus identi�cadores efectivos a través de las llamadas al sistema setu-id(), setresuid(), setfsuid(), setreuid(). De manera análoga para cambiar los identi�cadores de grupo,suplantando uid por gid en el nombre de las llamadas. La siguiente tabla muestra el efecto de estasllamadas sobre las credenciales del proceso.

Campo setuid (e) setresuid(u,e,s) setreuid(u,e)euid = 0 euid 6= 0

uid Cambia a e No cambia Cambia a u Cambia a u

euid Cambia a e Cambia a e Cambia a e Cambia a e

fsuid Cambia a e Cambia a e Cambia a e Cambia a e

Un dato crítico a saber es si el proceso no es root, estas llamadas solo tendrán éxito si el valora ser asignado está incluido en alguna de las otras credenciales actuales del proceso. Por ejemplo, un

Page 45: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 43

proceso puede almacenar el valor 511 en su campo fsuid invocando a la llamada setfsuid(), pero solopodrá hacerlo si alguna otra de la demás credenciales ya almacena este valor.

Consideremos la llamada sys_setuid(). Las acciones son diferentes dependiendo cuál es el euiddel proceso que invocó la syscall, o sea, 0 (el superusuario con privilegios) o un usuario normal. Sieuid es 0, la llamada asigna a las credenciales del proceso invocador el valor del parámetro e. Unproceso de superusuario puede dejar sus privilegios y transformarse en un proceso perteneciente a unusuario normal. Esto sucede, por ejemplo, como ya mencionamos, en la etapa de validación a travésdel programa login. Si el euid no es 0, setuid() modi�ca sólo los valores almacenados en euid y fsuid.

4.9.2. Capacidades de un proceso

POSIX.1e introdujo otro modelo de credenciales de proceso basado en la noción de �capacidades� ocapabilities. El kernel de Linux soporta las capacidades POSIX, aunque la mayoría de las distribucionesno las usan.

Una capacidad es, simplemente, una bandera que especi�ca si un proceso tiene permisos pararealizar cierta operación o un grupo especí�co de operaciones. Este modelo es diferente al enfoquetradicional de �superusuario versus usuario normal� en el cual un proceso podía hacer de todo oabsolutamente nada, dependiendo de su UID efectivo. Para citar algunas de las 31 capacidades queposee el kernel tenemos:

CAP_CHOWN → Ignora las restricciones cuando se quiere cambiar el dueño de un archivo.

CAP_FKILL → Sobrepasa los chequeos de permisos cuando se generan señales.

CAP_MKNOD → Permite operaciones mknod() privilegiadas.

CAP_SETUID → Ignora las restricciones cuando se quieren manipular las credenciales deusuario de un proceso.

CAP_SETGID → Ignora las restricciones cuando se quieren manipular las credenciales de grupode un proceso.

CAP_SYS_BOOT → Permite el uso de reboot().

CAP_SYS_MODULE → Permite insertar o remover módulos del kernel.

La principal ventaja de este modelo es que, en cualquier momento, cada programa necesita unnúmero limitado de capacidades. Un proceso puede, explícitamente, tomar y bajar su capacidad usandolas llamadas al sistema capget() y catset().

El kernel ya toma en cuenta este modelo. Consideremos, la syscall nice(), la cual permite a losusuarios cambiar la prioridad estática de un proceso. En el modelo tradicional, solo el superusuariopodía subir una prioridad; el kernel tenía que chequear si el campo euid de la task_struct del procesollamante era 0. Sin embargo, ahora el kernel de�ne una capacidad llamada CAP_SYS_NICE, que cor-responde exactamente con este tipo de operación. El kernel chequea el valor de esta bandera invocandola función capable() y pasando el valor de CAP_SYS_NICE a ésta.

Este enfoque funciona gracias a ciertos �hackeos� para lograr compatibilidad, que fueron adheridosen el código del kernel: cada vez que un proceso asigna 0 a sus campos euid y fsuid, el kernel asignatodas las capacidades posibles al proceso para que todos los chequeos tengan éxito. Cuando un procesoresetea los campos euid y fsuid con su UID real, el kernel chequea la bandera keep_capabilities en latask_struct y suelta todas las capacidades si la bandera no está activada. Un proceso puede activar odesactivar la bandera keep_capabilities por medio de la syscall prctl().

El modelo de capacidades esta fuertemente ligado con el framework LSM. Los hooks de seguridadestán almacenados en la tabla de tipo security_operations de�nida en include/linux/security.h. La di-rección de las funciones que implementan esos hooks son almacenadas en la variable security_ops. Por

Page 46: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 44

defecto, el kernel usa un modelo mínimo de seguridad implementado por la tabla dummy_security_ops,donde los hooks de esta tabla asociados a servicios del kernel que requieran un chequeo de capacidades,comprueban que el proceso que solicita el servicio posea la capacidad para seguir realizando la op-eración. El resto de los hooks de la tabla retorna incondicionalmente retorna 0 (operación permitida).

Por ejemplo, las rutinas stime() y settimeofday() invocan el hook de seguridad settime antes decambiar la fecha y hora del sistema. La función apuntada por la tabla dummy_security_ops sim-plemente chequea si la capacidad CAP_SYS_TIME del proceso actual está activada y retorna 0 o-EPERM acorde al resultado.

4.9.3. Init, Getty y Login

4.9.3.1. Init

El ancestro de todos los procesos es el llamado Proceso 0, idle process o el swapper. Este es unthread del kernel creado desde cero durante la etapa de inicialización de Linux y esta encargado derealizar funciones de scheduling.

La función start_kernel() en init/main.c completa la inicialización del kernel de Linux. Casi todoslos componentes son inicializados por esta función. Particularmente, el kernel thread para el Proceso1 es creado al invocar a la función kernel_thread(). Este nuevo thread tiene PID 1 y comparte con elswapper todas las estructuras que permiten al kernel manejar procesos. Cuando es seleccionado por elscheduler, el proceso init (PID 1) ejecuta la función init(). Esta función constituye la última etapa enla inicialización del kernel y, a lo último, llama a la syscall execve() para cargar el programa ejecutableinit. Normalmente, busca a este programa en las siguientes ubicaciones:

/sbin/init

/etc/init

/bin/init

/bin/sh

El último es un interprete de comandos para ejecutar init por nuestra cuenta en caso de que nosea encontrado en ninguna de las ubicaciones mencionadas. Si tampoco se encuentra el interprete, seemite kernel panic y el sistema queda inutilizable.

Como resultado de encontrar a init, el kernel thread creado se convierte en un proceso regular consus propias estructuras que no comparte con el swapper. Este proceso se mantiene vivo hasta que elsistema sea apagado porque es el encargado de crear y monitorear la actividad de todos los procesosque implementan las capas más externas del sistema operativo.

Normalmente, init usa un archivo de con�guración para saber qué procesos debe cargar y comoadministrarlos. Por ejemplo, en muchas distribuciones este archivo se encuentra en /etc/inittab. Tam-bién reconoce múltiples niveles de ejecución o runlevels. Un runlevel es una con�guración del softwaredel sistema que permite que exista solo un grupo de procesos. Se puede ver como un modo del sistema.Cada runlevel tiene características especiales que son controladas por los servicios que estén activosen cada nivel. Init puede correr el sistema en uno de ocho niveles de ejecución (hay dos que nunca seusan), los cuales sirven para diferentes propósitos:

0 - halt. Para el sistema para que pueda ser apagado.

1 - Modo de monousuario. Útil para mantenimiento del sistema.

2 - Multiusuario sin red. Activa las funcionalidades multiusuario, habilita los prompts de loginen terminales virtuales. Todo sin red.

Page 47: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 45

3 - Multiusuario con red. El nivel por defecto en la mayoría de las distribuciones. Igual que 2pero con servicios de red.

4 - no usado

5 - Servidor de ventanas. Como 3 pero, además, el servidor X es inicializado automáticamente.

6 - Reinicio. Como 0 pero el sistema reiniciará una vez que todo sea detenido.

La inittab determina en qué nivel de ejecución iniciar el sistema y qué procesos ejecutar en cadanivel. Una entrada tiene la forma:

id:runlevels:actions:process

Un ejemplo de inittab puede ser:

id:3:initdefault:

si::sysinit:/etc/rc.d/init.d/rc sysinit

l0:0:wait:/etc/rc.d/init.d/rc 0

l1:S1:wait:/etc/rc.d/init.d/rc 1

l2:2:wait:/etc/rc.d/init.d/rc 2

l3:3:wait:/etc/rc.d/init.d/rc 3

l4:4:wait:/etc/rc.d/init.d/rc 4

l5:5:wait:/etc/rc.d/init.d/rc 5

l6:6:wait:/etc/rc.d/init.d/rc 6

ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now

su:S016:once:/sbin/sulogin

1:2345:respawn:/sbin/agetty tty1 9600

2:2345:respawn:/sbin/agetty tty2 9600

3:2345:respawn:/sbin/agetty tty3 9600

4:2345:respawn:/sbin/agetty tty4 9600

5:2345:respawn:/sbin/agetty tty5 9600

6:2345:respawn:/sbin/agetty tty6 9600

La acción wait indica a init que espere a que el proceso detallado en la línea termine. respawn le exijea init que reinicie el proceso todas las veces que este muera.

Una vez ejecutadas todas las líneas de inittab, init realiza básicamente dos funciones:

Reejecutar a todos los procesos que así lo requieran.

Convertirse en padre de los procesos huérfanos del sistema.

En conclusión, el proceso init es pariente de todos los procesos del sistema. Controla cuáles procesosserán inicializados en tiempo de inicio del sistema y determina el �look and feel� del sistema una vezactivo.

4.9.3.2. Getty

El programa getty es ejecutado por init como parte del análisis de inittab. Su funcionalidad sepuede resumir en los siguientes tres items:

Abre las ttys y con�gura sus modos.

Imprime el prompt de login y toma el nombre de usuario.

Page 48: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 46

Empieza un programa login para el usuario ingresado en el item anterior.

Más detalladamente, getty lee el nombre de usuario e invoca al programa login con este nombre comoargumento. Mientras lee esta entrada, intenta adaptar la terminal al sistema y con�gura algunosparámetros de la terminal para favorecer el proceso de validación de los usuarios.

El dispositivo tty usado es determinado por el argumento en línea de comandos al ejecutarse getty.Normalmente, está explicitado en la inittab.

Por defecto, getty siempre ejecutará el programa ubicado en /bin/login, pero ofrece como opcióninvocar otro programa login, gracias a la opción -l program.

4.9.3.3. Login

El programa login personi�ca la última etapa en la validación de un usuario en el sistema y,básicamente, pide por una contraseña para validar al usuario que le fue pasado por getty en la línea decomandos. Si nada le fue pasado, entonces primero imprime su propio prompt, pide el nombre y luegola contraseña correspondiente. Por razones de seguridad, es típico que login permita como máximo 10intentos para chequear que la contraseña sea correcta. Además, a partir de la tercera prueba fallida, elprograma comienza a ponerse más y más lento. Una vez que el usuario haya sido validado, el programadesarrolla tareas de administración estándares. Estas incluyen:

Setear el UID y GID de la tty.

Preservar la variable de entorno TERM, si existe.

Preservar a todas las variables de entorno heredadas de getty e init, si la opción -p es usada.

Inicializa las variables de entorno HOME, PATH, SHELL, TERM, MAIL y LOGNAME.

Las rutas por defecto de los ejecutables del sistema (el PATH ) es inicializado, por ejemplo, a�/usr/local/bin:/bin/:/usr/bin� para usuarios normales.

El intérprete de comandos del usuario es ejecutado y, a menos que sea especi�cado, usará /bin/sh.Este programa se ejecutará con privilegios del usuario y no como root. Esto se logra gracias a lallamada al sistema sys_setuid.

Inicializa el directorio personal del usuario, especi�cado en /etc/passwd (ver sección siguiente),o usa / por defecto.

Otra función que realiza login es actualizar la contabilidad de los usuarios que los archivos /var/-run/utmp y /var/log/wtmp proporcionan. Estos contienen información sobre la cantidad de tiempoque los usuarios han permanecido en el sistema junto con sus ingresos y egresos. También getty e initpueden escribir en estos archivos.

Las contraseñas para cada usuario son guardadas, encriptadas, en el archivo /etc/passwd, a menosque se decida usar una herramienta para ocultarlas. La ocultación de contraseñas es una herramientaútil y una parte de la seguridad del sistema. Es un herramienta que permite proteger información delas contraseñas de aquellos que, realmente, no necesitan verla.

En pocas palabras, ocultar un �chero de contraseñas consiste en borrar las claves secretas codi�cadasen el archivo /etc/passwd (necesariamente legible por todos los usuarios) y situarlas en otro archivoque no pueda leer todo el mundo. Normalmente en los sistemas Linux, este otro �chero se llama/etc/shadow.

Page 49: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 47

El archivo /etc/passwdCon el usuario ya autenticado, login invoca al intérprete de comandos (shell), pero para saber qué

intérprete ejecutar buscará en el archivo /etc/passwd, el cual contiene entradas que de�nen completa-mente como se ejecutará este programa. Un ejemplo de cómo es este archivo es el siguiente:

root:x:0:0:root:/root:/bin/bash

bin:x:1:1:bin:/bin:

daemon:x:2:2:daemon:/sbin:

adm:x:3:4:adm:/var/adm:

lp:x:4:7:lp:/var/spool/lpd:

sync:x:5:0:sync:/sbin:/bin/sync

shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown

halt:x:7:0:halt:/sbin:/sbin/halt

mail:x:8:12:mail:/var/spool/mail:

news:x:9:13:news:/var/spool/news:

uucp:x:10:14:uucp:/var/spool/uucp:

operator:x:11:0:operator:/root:

games:x:12:100:games:/usr/games:

gopher:x:13:30:gopher:/usr/lib/gopher-data:

ftp:x:14:50:FTP User:/home/ftp:

nobody:x:99:99:Nobody:/:

xfs:x:100:101:X Font Server:/etc/X11/fs:/bin/false

gdm:x:42:42::/home/gdm:/bin/bash

postgres:x:40:233:PostgreSQL Server:/var/lib/pgsql:/bin/bash

squid:x:23:23::/var/spool/squid:/dev/null

pablo:x:500:500::/home/pablo:/bin/bash

samantha:x:501:501::/home/samantha:/bin/bash

madre:x:502:502::/home/madre:/bin/bash

La sintaxis es:

cuenta:contraseña:UID,GID,GECOS:directorio:shell

donde los campos son:

cuenta → El nombre del usuario.

contraseña → Una contraseña encriptada de usuario o un caracter si el sistema está usandocontraseñas al estilo shadow y las almacena en /etc/shadow.

UID → La identi�cación numérica del usuario.

GID → El número del grupo primario del usuario.

GECOS → Usualmente tiene el nombre completo del usuario. Opcional.

directorio → El path completo del directorio home del usuario.

shell → El path completo del shell del usuario.

Page 50: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 48

4.10. Teclas para servicios de seguridad

La Tecla de Atención Segura (Secure Attention Key o SAK) es una combinación especial de teclasa ser ingresada antes de que la pantalla de login sea presentada. Un ejemplo clásico en la combinaciónControl-Alt-Delete en los sistemas basados en Windows NT. Los usuarios deberían ser entrenadospara reportar a todo pantalla de login que aparezca sin haber presionado la SAK. Solo el kernelpuede detectar que la combinación de teclas de seguridad ha sido presionada. De esta manera, otrostipos de programas nunca pueden interceptar tal combinación. Esta característica hace que SAK seaespecialmente útil para implementar los caminos con�ables mencionados en la sección 2.2.2.

Para implementar SAK, Linux usa el sistema Magic SysReq Key. Este consiste en un combo �mági-co� de teclas, las cuales al ser pulsadas, producen una respuesta del kernel sin importar lo que el sistemaesté realizando en tal momento, a menos que esté totalmente tildado.

Esta es una funcionalidad que debe ser agregada al momento de con�gurar el kernel, y puede seractivada y desactivada a través del archivo /proc/sys/kernel/sysrq. Particularmente, en la arquitecturax86, sysrq se invoca presionando `ALT-Print Screen-<Tecla especí�ca>' y para citar algunas tenemos:

`b' → Reinicia inmediatamente el sistema sin sincronizar discos o desmontarlos.

`c' → Realiza un reinicio con kexec para poder hacer un crashdump.

`d' → Muestra todos lo locks que estén tomados.

`e' → Manda la señal SIGTERM a todos los procesos, excepto a init.

`h' → Muestra un help.

`i' → Manda la señal SIGKILL a todos los procesos menos a init.

`k' → SAK.

`o' → Apaga la computadora.

`u' → Trata de remontar a todos lo sistemas de archivos montados.

`0' - `9' → Cambia el nivel de logs de la consola, controlando qué mensajes el kernel podráimprimir en la consola.

Lo bueno es que este sistema además permite implementar nuevas funcionalidades a módulos delkernel, pudiendo asignarlas en alguna tecla disponible.

La tabla sysrq_key_table en kernel/sysrq.c contiene las diferentes sysrq_key_op correspondientesa cada tecla. Aquí se pueden observar cuáles están ocupadas estáticamente y cuáles no. La estructurasysrq_key_op (en include/linux/sysrq.h)

struct sysrq_key_op {

void (*handler)(int, struct tty_struct *);

char *help_msg;

char *action_msg;

int enable_mask;

};

contiene la información necesaria para realizar el respectivo servicio requerido, donde:

handler es la función que llevará a cabo el propósito de la tecla especí�ca.

help_msg es una cadena que describe lo que realiza la key. Este mensaje es impreso producto deinvocar a la sysrq `h'.

Page 51: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 49

action_msg es una cadena que será impresa apenas sea invocada la sysrq.

enable_mask es un número que indica si la esta sysrq es ignorada o no, dependiendo el valor quehaya en /proc/sys/kernel/sysrq.

Las funciones

int register_sysrq_key (int key, struct sysrq_key_op *op);

int register_sysrq_key (int key, struct sysrq_key_op *op);

proveen la interfaz para exportar teclas sysrq. La primera falla inmediatamente si la tecla requeridaestá ocupada.

Para manejar los pedidos, la función handle_sysrq() es invocada, generalmente, por el controladordel teclado una vez detectado el combo correspondiente a este tipo. La última tecla del combo espasada como argumento a esta rutina para saber qué servicio de la tabla mencionada iniciar.

4.11. Colas de trabajo

Las colas de trabajo (work queues) han sido introducidas en el kernel 2.6 y reemplazan una con-strucción similar llamada �task queue� usada en el Linux 2.4. Estas permiten que funciones del kernelsean activadas y luego, en otro momento, ejecutadas por kernel threads especiales llamados workerthreads.

Lo bueno de las colas de trabajo es que las funciones solicitadas correrán en contexto de proceso,a diferencia de otros tipos de construcciones, como las deferrable functions que lo hacen en contextode interrupción. Correr en contexto de proceso es la única forma de ejecutar funciones que se puedenbloquear, ya que ningún cambio de contexto puede llevarse a cabo durante una interrupción.

La estructura principal asociada con las colas de trabajo es el descriptor workqueue_struct. Estecontiene una estructura por CPU llamada cpu_workqueue_struct7 que contiene entre otras cosas:

Una lista, worklist, que contiene a los trabajos pendientes.

Una wait queue que permite al worker thread dormir mientras espera más trabajos.

El descriptor del proceso del worker thread de esta estructura.

La worklist contiene los trabajos que todavía le restan por procesar a la cola. Cada uno de estostrabajos es representado por la estructura work_struct.

Para utilizar las colas de trabajo hay un grupo de funciones de�nidas para su creación, destruccióny manejo:

create_workqueue() → Recibe un parámetro string y retorna la dirección de la workqueue_structdel la nueva work queue creada.

destroy_workqueue()→Destruye una work queue, recibiendo su worqueue_struct como parámetro.

queue_work() → Recibe una workqueue_struct y una work_struct, simbolizando un trabajo, yrealiza todas las tareas administrativas necesarias para registrarlo en la workqueue. Si el trabajoya existe, no lo agrega.

No es necesario, a veces, crear una work queue de cero. El kernel ofrece una prede�nida llamadaevents, la cual puede ser usada libremente por cualquier desarrollador. events contiene trabajos diferidosde muchas capas del kernel y controladores de E/S, y las funciones ejecutadas en ella no deberíanbloquearse por mucho tiempo, ya que la ejecución de las tareas pendientes en cada CPU es serializadoy, un retardo muy alto, produciría efectos negativos en los usuario de la cola.

7La razón para duplicar la workqueue por cada CPU es porque las estructuras de datos por procesador producen uncódigo más e�ciente

Page 52: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 50

4.12. Comunicación entre procesos (IPC)

El kernel de Linux posee medios a través de los cuales, diferentes threads en él pueden sincronizarpara usar datos compartidos o procesar alguna región crítica o incluso para comunicarse mutuamente.Sin embargo, también son necesarios mecanismos con los cuales programas de espacio de usuario puedangozar de dichas facilidades. Primitivamente, se puede lograr este requerimiento a través de un archivoregular. Dos procesos podrían compartir información escribiendo y leyendo del archivo y hasta bloqueary desbloquear el archivo (�le locking) para generar una especie de mecanismo de sincronización, peroeste enfoque es costoso debido a los múltiples accesos al disco. Por lo tanto, Linux ofrece un conjuntode llamadas al sistema que soportan la comunicación entre procesos sin interactuar con el sistema dearchivos. Los mecanismos que los sistemas Unix ofrecen son:

Pipes y FIFOs

Semáforos

Mensajes

Areas de Memoria Compartida

4.12.1. Pipes

El pipe (tubería) es un IPC (InterProcess Communication) provisto por todas las familias de Unix.Es un �ujo de información de un sentido entre procesos: toda los datos escritos por un proceso en unpipe es direccionado por el kernel a otro proceso, el cual, entonces, pueden leerlos.

Los pipes pueden ser considerados como archivos abiertos que no tienen ninguna imagen en lossistemas de archivos montados. Un proceso crea un nuevo pipe a través de la llamada a sistema pipe(),la cual retorna un par de descriptores de archivo; el proceso puede pasar tales descriptores a susdescendientes, a través de fork(), compartiendo el pipe con ellos. Los procesos pueden leer del pipe conla llamada sys_read() con el primer descriptor de archivo y pueden leer a través de sys_write() usandoel segundo descriptor.

Los pipes de�nido por POSIX tiene un solo sentido, por ende, si se necesitase un ida y vuelta deinformación, dos pipes deberán solicitarse.

Un pipe es implementado como un conjunto de objetos del VFS que no tiene una correspondienteimagen es disco. A partir de Linux 2.6, estos objetos están organizados en el sistema de archivosespecial pipefs para realizar su manejo. Como este sistema de archivos no tiene punto de montaje en elárbol de directorios del sistema, los usuario nunca lo ven. Sin embargo, gracias a pipefs, estos recursosde comunicación están completamente integrados en el VFS y el kernel puede tratarlos de la mismamanera que las FIFOs que, como veremos en la sección 4.12.2, son pipes pero que usan un archivoregular existente en disco para manejar el �ujo de la información.

4.12.1.1. Crear y borrar un pipe

La llamada al sistema pipe() invoca la función do_pipe() la cual:

1. Invoca a get_pipe_inode() para alojar e inicializar un objeto inodo en el sistema de archivospipefs. Particularmente, inicia una estructura particular para gestionar cada pipe denominadapipe_inode_info (de�nida en include/linux/pipe_fs_i.h) y la guarda en el campo i_pipe delinodo.

struct pipe_inode_info {

wait_queue_head_t wait;

unsigned int nrbufs, curbuf;

Page 53: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 51

struct page *tmp_page;

unsigned int readers;

unsigned int writers;

unsigned int waiting_writers;

unsigned int r_counter;

unsigned int w_counter;

struct fasync_struct *fasync_readers;

struct fasync_struct *fasync_writers;

struct inode *inode;

struct pipe_buffer bufs[PIPE_BUFFERS];

};

2. Aloja un objeto �le y un descriptor de archivo para el canal de lectura, asignándole el atributoO_RDONLY e inicializa el campo f_op del archivo (las �le operations) con la dirección de latabla read_pipe_fops.

3. De manera similar, crear un objeto �le con la banderaO_WRONLY y f_op igual a write_pipe_fopspara el canal de escritura.

4. Aloja un objeto dentry (ver 4.14.3) y lo utiliza para enlazar the los objetos �les y el objeto inodo(ver 4.14.2).

5. Retorna los dos descriptores de archivos al proceso en espacio de usuario.

Cada vez que un proceso abre un pipe se toma una referencia al objeto �le que abrió. Cuandodicha referencia llegue a cero, signi�ca que ningún proceso utiliza más el pipe y se invoca a las rutinaspipe_read_release() o pipe_write_release(), dependiendo de que extremo de la tubería se este liberan-do. Cualquiera de estas dos funciones chequea si el otro extremo ya fue liberado, y en caso a�rmativo,invoca a pipe_release() para remover toda la memoria utilizada por el recurso.

4.12.1.2. Leer y Escribir a un pipe

Como el pipe está completamente integrado en el VFS, la llamadas read() y write() son las dosinterfaces principales que un proceso dispone para manipular u obtener información de este recurso.Luego de los chequeos necesarios, las rutinas sys_read() y sys_write() invocan a las rutinas de menornivel asociadas a cada operación respectiva y cuya ubicación fue almacenada previamente en el campof_op por do_pipe(). Las funciones pipe_read() y pipe_write() constituyen estas rutinas de menor nively se encargan de manejar el trá�co de datos de cada pipe. La lógica es la siguiente:

Cada tubería posee una serie de 16 bu�ers del tamaño de un página de memoria cada uno. Siun proceso quiere escribir y todos los bu�ers están llenos, debe esperar y dormir en la cola deespera de�nida en el campo wait de la estructura pipe_inode_info.

De manera análoga, si ningún bu�er contiene información y un proceso desea leer del pipe,también debe dormir.

En caso que la operación particular se haya requerido como no bloqueante (O_NONBLOCK ), elproceso no duerme y retorna inmediatamente.

4.12.2. FIFOs

Aunque los pipes con más simples, �exibles y e�cientes mecanismos de comunicación, tiene unadesventaja principal: no hay forma de abrir un pipe ya existente. Esto hace que sea imposible que dos

Page 54: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 52

procesos arbitrarios puedan compartir la misma tubería. Salvo que el recurso haya sido creado por unancestro en común.

Para solucionar tal limitación, los sistema Unix introdujeron un tipo especial de archivo (ver 4.6.3)denominado pipe nombrado (named pipe) o FIFO (First In - First Out ; el primer byte escrito en elarchivo especial es también el primer byte que será leído). Cada FIFO es como un pipe: en vez decontener bloques de disco en el sistema de archivos, una FIFO es asociada con un bu�er del kernel quetemporalmente almacena información intercambiada por dos o más procesos.

Gracias al inodo de disco, una FIFO puede ser accedida por todo proceso porque el nombre dearchivo del recurso está en el árbol de directorios del sistema.

En Linux 2.6, las FIFOS y los pipes son casi idénticos y usan la misma estructura pipe_inode_info.Además los métodos de lectura y escritura de los pipes nombrados son también implementados a travésde la funciones pipe_read() y pipe_write() respectivamente. Actualmente, hay solo dos diferenciassigni�cativas:

Los inodos de un FIFO aparecen en el árbol de directorios del sistema en vez de en el sistema dearchivos pipefs.

Las FIFOS con un canal de información bidireccional, o sea, es posible abrir una FIFO en modelectura/escritura.

Por último, cabe mencionar que al igual que todo archivo especial, las FIFOS son creadas a través dela llamada a sistema mknod(). Como vimos en la sección 4.6.3, la función ramfs_mknod() aloja e inicial-iza un inodo para el nuevo archivo especial, invocando posteriormente a la rutina init_special_inode()para completar la inicialización. Particularmente, si el inode corresponde a una FIFO (inode->i_mode& S_IFIFO), asigna al campo i_fop de éste para que apunte a def_�fo_fops. Esta estructura de�neun rutina open(), �fo_open(), la cual inicia el campo f_op del archivo con algunas de la siguientesestructuras:

FIFO de solo lectura read_�fo_fops.

FIFO de solo escritura write_�fo_fops.

FIFO de lectura/escritura rdwr_�fo_fops.

Tanto para read_�fo_fops como para write_�fo_fops se de�nen los métodos bad_pipe_w() ybad_pipe_r() los cuales devuelven error al querer realizarse una operación que no puede realizarse porel modo actual en que fue abierta la FIFO.

4.12.3. System V IPC

El término IPC comúnmente se re�ere al conjunto de mecanismos que permiten a un proceso enespacio de usuario hacer lo siguiente:

Sincronizarse con otros procesos a través de semáforos.

Enviar mensajes a otros procesos o recibir mensajes de ellos.

Compartir un área de memoria con otros procesos.

Los tres tipos de IPC conocidos como System V IPC que proveen los mencionados mecanismosson:

Semáforos System V.

Colas de mensajes System V.

Page 55: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 53

Memoria compartida System V.

El término System V es usado para este tipo de recursos dada su herencia del Unix System V y sediferencian del tipo de recursos IPC de�nidos por el estándar POSIX los cuales trataremos en la sección4.12.4. El kernel de Linux soporta los tres mecanismos de comunicación de System V, mientras que soloofrece una implementación de colas de mensajes para POSIX dejando que el resto sea implementadoen librerías de espacio de usuario8.

Las estructuras de datos IPC son creadas dinámicamente cuando un proceso requiere un recursoIPC. Este recurso es persistente y, a menos que sea explícitamente removido por un proceso, es con-servado en memoria y se encuentra disponible hasta que el sistema sea apagado. A diferencia de lospipes, un recurso IPC puede ser compartido por muchos procesos, aún por aquellos que no poseen unancestro en común.

Dado que un proceso puede requerir varios IPC del mismo tipo, cada recurso es identi�cado por unregistro de 32 bits al que denominaremos IPC key, el cual es similar al nombre de ruta de un archivoen el árbol de directorios del sistema. Además, cada IPC tiene un identi�cador de 32 bits, el cual puedeverse como el descriptor de archivo asociado a un archivo abierto. Los identi�cadores son asignados arecursos IPC por el kernel y son únicos dentro del sistema, mientras que las keys pueden ser escogidaslibremente por los programadores.

Cuando dos o más procesos quieran comunicarse a través de un recurso IPC, todos deben referirseal identi�cador del recurso.

Un recurso IPC es creado al invocar a las llamadas al sistema semget(), msgget() o shmget()dependiendo si se quiere un semáforo, una cola de mensajes o un área de memoria compartida, respec-tivamente. Generalmente, estas funciones reciben como parámetro una IPC key, un modo de aperturay:

Chequean si la key no contiene el valor IPC_PRIVATE. Si lo tiene, alojan el recurso requeridoy devuelven su identi�cador sin realizar más chequeos.

Si la key no contiene el valor IPC_PRIVATE, deben comprobar si ya no se ha creado un recursoIPC para la misma key.

• Si ya se ha hecho y el modo de apertura no requiere que se cree un recurso nuevo para la key(IPC_CREAT | IPC_EXCL), entonces se devuelve el identi�cador del IPC previamentecreado.

• Si no se ha hecho y el modo de apertura especi�ca que si el recurso no se encuentra se debecrear uno nuevo (IPC_CREAT ), se crea.

Si dos procesos independientes quisieran compartir el mismo recurso IPC lo pueden hacer de dosmaneras:

Los procesos concuerdan en usar una key �ja y prede�nida. Un problema puede surgir cuandootro proceso justo elige la misma key para otro recurso.

Un proceso invoca a las llamadas al sistema semget(), msgget() o shmget() especi�cando el valorIPC_PRIVATE en la key y comunica al otro proceso (a través de otro medio de comunicación)el identi�cador o crea otro proceso a través de fork().

Cada tipo de recurso IPC posee un estructura de datos denominada ipc_ids la cual, principalmente,incluye información sobre la cantidad de recursos alojados del tipo y un arreglo donde se ubican cadauno de estos. Un recurso IPC particular está asociado con la estructura kern_ipc_perm:

8Para más detalles de cómo implementar mecanismos IPC del tipo POSIX ver [17].

Page 56: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 54

struct kern_ipc_perm

{

spinlock_t lock;

int deleted;

key_t key;

uid_t uid;

gid_t gid;

uid_t cuid;

gid_t cgid;

mode_t mode;

unsigned long seq;

void *security;

};

que contiene campos para registrar el dueño de un recurso, su key y principalmente un puntero LSMen donde agregar atributos de seguridad.

Las llamadas al sistema semctl(), msgctl() y shmctl() permiten realizar consultas sobre un recursoen particular, dado su identi�cador. Los datos obtenidos varían desde la cantidad de mensajes quepuede tener alojados actualmente una cola hasta la cantidad de bytes que ocupa un área de memoriacompartida o el estado de un semáforo. Particularmente, cuando un proceso desea remover un recursoIPC invoca a estas llamadas con la bandera IPC_RMID como argumento. Esto es común para todoslos recursos. Sin embargo, hay operaciones relativas a cada uno y de las cuales nos centraremos en lassecciones siguientes.

4.12.3.1. Semáforos

Los semáforos IPC son contadores usados para proveer acceso controlado a estructuras de datoscompartidas por múltiples procesos.

El valor del semáforo es positivo si el recurso protegido está disponible y 0 si no lo está. Un procesoque quiera acceder al recurso, intenta decrementar el valor del semáforo y el kernel bloquea al procesohasta que mientras el valor no sea positivo. Cuando el proceso deja de usar el recurso, incrementa elvalor de semáforo y, al hacerlo, cualquier proceso esperando al semáforo es despertado.

Cada semáforo IPC es un conjunto de uno o más valores de semáforos, no uno solo como lossemáforos convencionales que el kernel usa para controlar la concurrencia de threads del kernel. Estoimplica que un solo recurso IPC puede proteger a más de una estructura de datos compartida. Elnúmero de �mini semáforos� que se requieren para un semáforo IPC son especi�cados en la llamadasemget().

Los semáforos System V proveen un mecanismo de seguridad para situaciones en las cuales unproceso muere sin deshecho las operaciones realizadas previamente sobre un semáforo. Cuando unproceso escoge usar este mecanismo, la operaciones resultantes son denominadas desechables. Cuandoel proceso muere, todos sus semáforos IPC puede retroceder y quedar en el valor que hubiesen tenidosi el proceso nunca se hubiese ejecutado.

La estructura del kernel que identi�ca a un semáforo IPC se denomina sem_array de�nida eninclude/linux/sem.h.

struct sem_array {

struct kern_ipc_perm sem_perm; /* permisos */

int sem_id; /* identificador IPC */

time_t sem_otime; /* tiempo última semop */

time_t sem_ctime; /* tiempo último cambio */

struct sem *sem_base; /* ptr al primer mini semáforo */

Page 57: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 55

struct sem_queue *sem_pending; /* operaciones pendientes */

struct sem_queue **sem_pending_last; /* última operación pendiente */

struct sem_undo *undo; /* pedidos de desechar */

unsigned long sem_nsems; /* no. de mini semáforos */

};

La variable sem_ids almacena la estructura ipc_ids para el recurso IPC de tipo semáforo. Estavariable contiene el arreglo de semáforos actualmente creados y cada semáforo contiene un arreglo demini semáforos que lo componen. Normalmente, se denomina como arreglo de semáforos al recursoIPC y semáforo en sí a cada mini semáforo que contenga el primero. La estructura que identi�ca en elkernel a estos último se incluye a continuación:

struct sem {

int semval; /* valor actual */

int sempid; /* pid de la última operación */

};

Para incrementar o decrementar el valor de cada semáforo se utiliza la llamada al sistema se-mop(). Esta recibe un identi�cador, un conjunto de semáforos y las operaciones a realizar en cadauno. Para obtener al recurso correspondiente al identi�cador argumento, sys_semop() invoca a la ruti-na sem_lock(), la cual a su vez invoca a la función ipc_lock(). Esta función, de�nida en ipc/util.c,busca en la variable sem_ary por el recurso que corresponda al identi�cador y lo protege contra unacceso concurrente en el futuro. Al �nalizar devuelve la estructura kern_ipc_perm del recurso paraque luego, sem_lock() transforme este dato en uno de tipo sem_array y así poder trabajar sobre elsemáforo objetivo.

Al realizar un IPC_RMID se realizar un proceso similar con la diferencia de que la funciónipc_rmid() es invocada en vez de ipc_lock. Esta quita al recurso del arreglo en sem_ary e iniciael campo deleted en la kern_ipc_perm para simbolizar que el recurso esta en proceso de destruccióny que cualquier invocación a la llamada semget() o la función sem_lock() debe retorna como si elrecurso no existiese.

4.12.3.2. Colas de Mensaje

Los procesos pueden comunicarse entre ellos por medio de mensajes. Cada mensaje generado porun proceso es enviado a una Cola de mensaje IPC, en donde es guardado hasta que otro proceso lo lee.

Un mensaje se compone de una cabecera de tamaño �jo y una parte de texto de tamaño variable.Además puede ser etiquetado con un valor entero (tipo del mensaje) que permite a un proceso retornamensajes selectivamente de una cola dada. Una vez que un mensaje es leído de la cola, el kernel lodestruye, por lo que, solo un proceso puede obtener a mensaje dado.

Para mandar un mensaje un proceso invoca a la llamada al sistema msgsnd() pasando:

El identi�cador IPC de la cola de mensaje destino.

El tamaño del mensaje.

La dirección de un bu�er en espacio de usuario que contenga el tipo del mensaje seguido por elcontenido de éste.

Para obtener un mensaje un proceso invoca a la llamada al sistema msgrcv() pasando:

El identi�cador IPC de la cola de mensaje fuente.

Un puntero a un bu�er de espacio de usuario en donde se copiarán el tipo y el contenido demensaje.

Page 58: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 56

El tamaño del bu�er.

Un valor t que especi�ca que tipo de mensaje debe ser obtenido.

Al igual que los semáforos, las colas de mensajes poseen una variable msg_ids que contiene laestructura ipc_ids para su tipo de IPC. Cada cola de mensaje está representada por la estructuramsg_queue de�nida en include/linux/msg.h:

struct msg_queue {

struct kern_ipc_perm q_perm; /* permisos */

int q_id; /* identificador IPC */

time_t q_stime; /* tiempo último msgsnd */

time_t q_rtime; /* tiempo último msgrcv */

time_t q_ctime; /* tiempo último cambio */

unsigned long q_cbytes; /* no actual de bytes en la cola */

unsigned long q_qnum; /* no de mensajes en la cola */

unsigned long q_qbytes; /* no máximo de bytes en la cola */

pid_t q_lspid; /* pid del último msgsnd */

pid_t q_lrpid; /* pid del último msgrcv */

struct list_head q_messages; /* lista de mensaje en la cola */

struct list_head q_receivers; /* receptores esperando */

struct list_head q_senders; /* emisores esperando */

};

La rutinamsg_lock() obtiene la estructuramsg_queue correspondiente al identi�cador IPC a travésde la función ipc_lock().

La cola puede alojar tantos mensajes como quiera siempre y cuando no se sobrepase la el tamañomáximo de una cola indicado en el campo q_qbytes de ésta. Este valor puede ser cambiado a travésde la llamada msgctl().

Un mensaje particular es representado por la estructura msg_msg y lo interesante es que posee uncampo especial LSM para poder alojar atributos de seguridad en él.

struct msg_msg {

struct list_head m_list;

long m_type; /* tipo del mensaje */

int m_ts; /* tamaño del texto del mensaje */

struct msg_msgseg* next; /* próximo segmento del mensaje */

void *security;

/* El contenido del mensaje se ubica a continuación... */

};

Cuando se aloje un nuevo mensaje debe pedirse memoria para la estructura msg_msg y para elcontenido del mensaje. Como esta memoria es contigua, el contenido se encontrará inmediatamentedespués de la estructura msg_msg.

Para mensajes que son más grandes que 4,072 bytes (tamaño de página menos el tamaño deldescriptor del mensaje) se van alojando nuevos segmentos que contienen la dirección de la próximapágina en donde se ubicará el mensaje. Estos se encuentran enlazados por el campo next y su estructuraestá de�nida como sigue:

struct msg_msgseg {

struct msg_msgseg* next; /* próximo segmento */

/* La parte que sigue del contenido del mensaje

Page 59: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 57

se ubica a continuación... */

};

Por último, si la cola está llena o vacía, los emisores o receptores de mensajes deberán dormiren ella hasta que uno de estos respectivos estados cambie. Los campos q_senders y q_receivers de laestructura msg_queue están destinados a alojar a estos procesos durmientes. Si hay procesos esperandoun mensaje y un nuevo emisor aparece, no se guarda el mensaje en la cola, sino que se envía el mensajedirectamente a receptor esperando.

4.12.3.3. Memoria Compartida

El mecanismo IPC más poderoso en la memoria compartida, la cual permite a dos o más procesospuedan acceder a estructuras de datos comunes al ubicarlas en una región de memoria compartida IPC.Todo proceso que quiera acceder a esta estructuras de datos debe agregar a su espacio de direcciones(address space) una nueva región que se ligue con los marcos de memoria asociados a la región dememoria compartida IPC. Tales marcos pueden ser controlados fácilmente por el kernel a través lapaginación por demanda o demand paging. La sección 4.13 resume brevemente cómo Linux administrala memoria del sistema y su lectura puede facilitar a la mejor comprensión de este recurso IPC.

La llamada shmat() es invocada para adjuntar una región de memoria IPC (previamente alojada porshmget()) a un proceso. Recibe como parámetro el identi�cador IPC de la región e intenta agregarla alespacio de direcciones del proceso invocador. El proceso puede especi�car el comienzo de una direcciónlineal para la región de memoria, pero la dirección no es de mucha importancia y cada proceso queutilice la región puede usar diferentes direcciones en su propio espacio de memoria.

La llamada shmdt() es invocada para quitar una región de memoria compartida IPC especi�cadapor su identi�cador, o sea, remover la región de memoria correspondiente del espacio de direcciones delproceso. Recordar que los recursos IPC son persistentes, aún cuando ningún proceso los está usando.Por lo tanto, las paginas del área compartida no pueden ser descartadas, pero pueden ser intercambiadasdesde la memoria física a la memoria de intercambio.

Como en los otros IPC, el tipo de memoria compartida posee una variable llamada shm_ids, detipo ipc_ids, donde se ubican todas las regiones solicitadas. Estas están representadas por la estructurashmid_kernel, de�nida en include/linux/shm.h y detallada a continuación:

struct shmid_kernel /* private to the kernel */

{

struct kern_ipc_perm shm_perm; /* permisos */

struct file *shm_file; /* archivo especial del segmento */

int id; /* identificador IPC */

unsigned long shm_nattch; /* no de procesos con el segmento */

unsigned long shm_segsz; /* tamaño del segmento */

time_t shm_atim; /* tiempo de último shmat */

time_t shm_dtim; /* tiempo de último shmdt */

time_t shm_ctim; /* tiempo de último cambio */

pid_t shm_cprid; /* pid del creador */

pid_t shm_lprid; /* pid del último shmat */

struct user_struct *mlock_user;

};

El campo más importante es shm_�le, el cual almacena la dirección de un objeto �le. Esto re�ejala integración de la memoria compartida IPC con la capa VFS en Linux 2.6 (ver sección 4.14.2). Enparticular, cada recurso de memoria compartida es asociado a un archivo perteneciente al sistema dearchivos especial shm. Como shm no tiene punto de montaje en el árbol de directorios del sistema,ningún usuario puede abrir ni acceder sus archivo por medio de llamadas al sistema del VFS. Sin

Page 60: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 58

embargo, cuando un proceso adjunta un segmento, el kernel invoca a la función do_mmap() y creaun nuevo mapeo9 de memoria compartida del archivo en el espacio de direcciones del proceso. Por lotanto, los archivos pertenecientes al sistema de archivos shm tiene solo un método de�nido, mmap, elcual es implementado por la función shm_mmap() en mm/shmem.c.

Al igual que los semáforos y las colas de mensaje, existe una función llamada shm_lock(), quien através de ipc_lock(), traduce un identi�cador IPC a su correspondiente estructura shmid_kernel a lahora de adjuntar, remover o realizar alguna operación sobre un área de memoria compartida.

En la sección 4.13 se ahondarán más detalles de bajo nivel sobre este tipo de recurso IPC, peroconocimientos previos de cómo administra Linux la memoria son necesarios.

4.12.4. Colas de Mensajes POSIX

La comunicación entre procesos de�nida por el estándar POSIX es una variante de los IPC deSystemV. También de�ne mecanismos para sincronizar procesos (semáforos), enviar y recibir mensajes (colasde mensajes) y compartir regiones de memoria (memoria compartida).

Solo las colas de mensajes POSIX son soportadas y mantenidas por el kernel desde el kernel 2.6.6,mientras que los otros dos mecanismos de comunicación son implementados como librerías usando losIPC de System V u otras llamadas al sistema provistas por el kernel (ej. mmap()).

Las colas de mensajes POSIX son muy parecidas a las de System V, pero poseen a número deventajas sobre estas últimas:

Una interfaz basada en archivos más simple para las aplicaciones.

Soporte nativo para las prioridades en los mensajes (las prioridad determina la posición delmensaje en la cola).

Soporte nativo para la noti�cación asíncrona de la llegada de mensajes, por medio de señales ocreación de threads del kernel.

Bloqueos temporizados en los envíos y recepciones.

Las colas POSIX son manejadas por un conjunto de funciones de librerías, la cuales centran sufuncionalidad en las siguientes llamadas al sistema:

mq_open()

Abre (opcionalmente crea) una cola de mensajes POSIX.

mq_unlink()

Destruye un cola de mensajes POSIX.

mq_timedsend()

Envía un mensaje a una cola de mensajes POSIX de�niendo un tiempo límite para la operación.Si este tiempo es 0, se espera inde�nidamente hasta que haya espacio en la cola.

mq_timedreceive()

Obtiene un mensaje de una cola de mensajes POSIX de�niendo un tiempo límite para la op-eración. Si este tiempo es 0, se espera inde�nidamente hasta que haya algún mensaje para re-tornar.

9La palabra mapear es muy usada en la jerga de la computación y hace referencia a la acción de establecer un enlaceuno a uno entre dos entidades.

Page 61: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 59

mq_notify()

Establece un mecanismo de comunicación asíncrono para la llegada de mensajes en una colavacía.

mq_getsetattr()

Retorna y asignar nuevos atributos a una cola de mensajes POSIX (esencialmente, si un envío ouna recepción deben ser bloqueantes o no).

La implementación de las colas de mensajes POSIX en Linux 2.6 es simple y directa. Un sistema dearchivos especial denominado mqueue ha sido introducido y contiene un inodo por cada cola existente.Las llamadas al sistema mencionadas actúan transparentemente en los archivos de sistema de archivosmqueue, por lo que la mayoría del trabajo es implementado por el VFS (ver sección 4.14.2). Por ejemplo,notar que el kernel no ofrece una llamada al sistema mq_close(); de hecho, el descriptor de la colaretornado a una aplicación es efectivamente un descriptor de archivo, por lo tanto la función de libreriamq_close() puede simplemente ejecutar la llamada close() para realizar su trabajo.

El sistema de archivos mqueue no tiene que necesariamente ser montado en el árbol de directoriosdel sistema. Sin embargo, si es montado, un usuario puede crear un cola POSIX creando un nuevoarchivo en el directorio raíz de mqueue. Además, puede obtener información sobre la cola leyendo elarchivo correspondiente. Por último, la aplicación puede invocar operaciones poll() o notify() para sernoti�cado sobre cambios en el estado de la cola.

Cada cola de mensajes es representada por el descriptor mqueue_inode_info de�nido en ipc/-mqueue.c como sigue:

struct mqueue_inode_info {

spinlock_t lock;

struct inode vfs_inode;

wait_queue_head_t wait_q;

struct msg_msg **messages;

struct mq_attr attr;

struct sigevent notify;

struct pid* notify_owner;

struct user_struct *user; /* usuario que la creo */

struct sock *notify_sock;

struct sk_buff *notify_cookie;

/* para procesos esperando espacio libre y mensajes, respectivamente */

struct ext_wait_queue e_wait_q[2];

/* tamaño de la cola en memoria (suma de todos los mensajes) */

unsigned long qsize;

};

Esta estructura recubre al objeto inodo asociado al archivo de la cola de mensajes. Cuando unade las llamada al sistema de las colas de mensajes POSIX recibe un descriptor de una cola comoparámetro, invoca la función fget() para derivar la dirección del objeto �le correspondiente; luego, lallamada toma el objeto inode del �le y, a través de la función container_of() obtiene el descriptormqueue_inode_info de la cola.

Los mensajes pendientes a ser recolectados son enlazados en un lista de la misma forma en quelo eran en las colas de mensajes System V. Cada mensaje también es representado por la estructuramsg_msg descrita en la sección 4.12.3.2.

Page 62: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 60

4.13. La Memoria en Linux

El manejo de la memoria es por lejos la actividad más complicada en un kernel de Unix. Todoslos sistemas Unix recientes proveen una útil abstracción denominada memoria virtual. La memoriavirtual actúa como una capa lógica entre los pedidos de memoria por parte de aplicaciones y la Unidadde Manejo de Memoria del hardware (MMU Memory Management Unit). La memoria virtual tienemucho propósitos y ventajas:

Muchos procesos pueden ejecutar concurrentemente.

Es posible correr aplicaciones cuyas necesidades de memoria sean mayores que la memoria físicadisponible.

Los procesos pueden ejecutar un programa cuyo código solo ha sido parcialmente cargado enmemoria.

Se le permite a cada proceso acceder solo un subconjunto de la memoria física disponible.

Los procesos pueden compartir una imagen de memoria de una librería o programa.

Los programas pueden ser desplazados a cualquier lugar de la memoria física.

Los programadores pueden escribir código independiente de la máquina, ya que no necesitanpreocuparse acerca de la organización de la memoria física.

El componente principal del subsistema de memoria virtual es la noción de espacio de direccionesvirtual. El conjunto de referencia a memoria que un proceso puede usar es diferente a las direccionesde la memoria física. Cuando un proceso utiliza una dirección virtual (o dirección lineal), el kernely la MMU cooperan para encontrar la actual locación física (dirección física) del item de memoriarequerido.

Hoy en día, el CPU incluye circuitos de hardware que traducen automáticamente direcciones vir-tuales en físicas. Para ese propósito, la RAM disponible es particionada en marcos de memoria opáginas de, típicamente, 4 a 8 KB y un conjunto de Tablas de Páginas (Page Tables) es introducidopara especi�car cómo las direcciones virtuales corresponden a las direcciones físicas. Este circuito haceal alojamiento de memoria simple, ya que un pedido para un bloque de direcciones virtual contiguaspueden ser satisfecho alojando un grupo de páginas que tengas direcciones físicas no contiguas.

Linux 2.6 adopta un modelo de paginación que encaja en las arquitecturas de 32 y 64 bits. Elmodelo es de 4 niveles y consta de 4 tipos de tablas de páginas, como muestra la Figura 4.2:

Directorio Global de Páginas (Page Global Directory).

Directorio Superior de Páginas (Page Upper Directory).

Directorio Medio de Páginas (Page Middle Directory).

Tabla de Páginas (Page Table).

El Directorio Global incluye las direcciones de varios Directorios Superiores, quien a su vez, incluyedirecciones de varios Directorios Medios, quien a su vez incluye direcciones de varias Tablas de Páginas.Cada entrada de una Tabla de Páginas apunta a un marco de memoria. Para algunas arquitecturasno hace falta tanta división en la dirección lineal hasta se prescinda del Directorio Medio o hasta delSuperior.

El manejo de los procesos en Linux se apoya mucho en la paginación. De hecho, el diseño de latraducción de direcciones sigue los siguiente objetivos:

Page 63: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 61

Figura 4.2: Modelo de paginación en Linux.

Asignar espacios de direcciones físicas diferentes a cada proceso, asegurando una proteccióne�ciente contra errores de direccionamiento.

Distinguir a las páginas (grupos de datos) de los marcos de memoria o páginas físicas (direccionesfísicas en la memoria principal). Esto permite que la misma página a ser almacenada en un marcode memoria pueda ser guardada en disco y luego recargada nuevamente en otro marco.

El registro de control cr3 contiene la dirección inicial del conjunto de Tablas de Páginas de unproceso. Este es guardado y restaurado en cada cambio de contexto en el descriptor del proceso. Porlo tanto, cuando un proceso resume su ejecución en el CPU, la unidad de paginación referencia alconjunto de tablas correcto.

4.13.1. Manejo de la Tabla de Páginas

Las estructuras pte_t, pmd_t, pud_t y pgd_t describen el formato de, respectivamente, una Tablade Páginas, un Directorio Medio, un Directorio Superior y un Directorio Global de Páginas. Estasestructura están de�nidas en include/asm-i386/page.h:

typedef struct { unsigned long pte_low; } pte_t;

Las entradas de este conjunto de Tablas tiene la misma estructura. Una entrada incluye los sigu-ientes campos:

Bandera Present

Si esta activada la página referenciada está contenida en la memoria principal. De lo contrario,la página no está contenida en la memoria principal y la unidad de paginación debe grabar ladirección lineal en el registro de contrl cr2 y generar un excepción de Fallo de Página (PageFault) para que el kernel inserte la página en la memoria. Ver sección 4.13.5.

Page 64: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 62

Bandera Accesed

Se activa cada vez que la unidad de paginación direccionada el marco de memoria correspondiente.Esta bandera es usada por el sistema operativo para seleccionar páginas para ser barridas de lamemoria. Ver sección 4.13.8.

Bandera Dirty

Solo se aplica a entradas de la Tabla de Páginas. Se activa cada vez que una operación de escrituraes realizada sobre el correspondiente marco de memoria. La unidad de paginación nunca desactivaesta bandera; el sistema operativo debe hacerlo. Sirve también para el proceso de barrido.

Bandera Read/Write

Contiene los derechos de acceso de la página o la Tabla de Páginas. Pueden suceder que se intenteescribir a una página de solo lectura, lo que genera también una excepción de Fallo de Página.

Bandera User/Supervisor

Contiene los niveles de privilegios requeridos para acceder a la página o la Tabla de Páginas.

Bandera Page Size

Solo se aplica a los Directorios de Páginas. Si está activa, la entrada referencia un marco de 2 o4 MB.

Bandera Global

Solo se aplica a las entradas de un Tabla de Páginas. Previene que la página sea limpiada delcache TLB. Ver sección 4.13.2.

Bandera PCD y PWT

Controla la manera en que la página y Tabla de Páginas es manejada por el cache del Hardware,el cual se encarga de guardar referencias a marcos de memoria para alivianar el gran diferencialde velocidad que existe entre el bus de memoria y el CPU.

Dirección Física

Este campo contiene los 20 bits más signi�cativos de la dirección física del marco de memoriaasociado. Como cada marco tiene 4 KB de capacidad, su dirección física debe ser múltiplo de4096, por lo tanto, los 12 bits menos signi�cativos son siempre 0.

El kernel provee macros para leer o modi�car entradas del conjunto de tablas. Algunas de las másútiles son:

pte_none,pmd_none, pud_none y pgd_none devuelven 1 si la entrada correspondiente tiene elvalor 0, o sea, no ha sido aún inicializada.

pte_clear,pmd_clear, pud_clear y pgd_clear limpia una entrada del correspondiente a un con-junto de tablas, por lo que prohíbe al proceso usar la dirección lineal mapeada por la entrada.

pte_present devuelve 1 si la bandera Present de la entrada de una Tabla de Páginas es igual a1, caso contrario devuelve 0.

Además provee funciones para consultar las banderas de cada página:

pte_user() Bandera User/Supervisor.

pte_write() Bandera Read/Write.

Page 65: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 63

pte_dirty() Bandera Dirty.

pte_young() Bandera Accessed.

pte_�le() Bandera Dirty (Cuando Dirty está activa y Present no, la página pertence a unmapeo de archivo de disco, concepto explicado en la sección 4.13.4.1).

Otro grupo de funciones son también de�nidas en include/asm-i386/pgtable.h para manipular estasbanderas.

Para �nalizar, existen un conjunto de macros útiles para inferir, dada una dirección lineal, cadauno de sus Directorios de Páginas, su Tabla de Páginas y hasta la página en si, representado en elkernel por la estructura page. Algunas de estas macros son:

pgd_o�set(mm, addr)

Devuelve la dirección lineal de la entrada en el Directorio Global de Páginas que corresponde alas dirección addr. El Directorio es encontrado a través del puntero al descriptor de memoria delproceso, mm. Este descriptor será introducido en la sección 4.13.4.

pud_o�set(pgd, addr)

Idem pgd_o�set() pero devuelve la entrada en el Directorio Superior dada una entrada en elDirectorio Global.

pmd_o�set(pud, addr)

Idem pud_o�set() pero devuelve la entrada en el Directorio Medio dada una entrada en elDirectorio Superior.

pte_o�set_map(dir, addr)

Idem pgd_o�set() pero devuelve la entrada en la Tabla de Páginas dada una entrada en elDirectorio Medio, dir.

pte_page(x)

Devuelve la dirección del descriptor de página referenciado por la entrada x de la Tabla dePáginas.

La estructura page está asociada a páginas físicas y no a páginas virtuales. Aún cuando la in-formación en una página sigue existiendo (ya sea en memoria o en el disco), puede ocurrir que éstano este asociado con el mismo descriptor page en todo momento. El kernel usa esta estructura paradescribir a lo que sea que esté almacenado en el marco de memoria en un momento dado. Por lo tanto,el objetivo del descriptor es describir un marco de memoria física y no a la información contenida enél. La estructura está de�nida en include/linux/mm_types.h de la siguiente manera:

struct page {

unsigned long flags; /* banderas */

atomic_t _count; /* conteo de usos del marco de memoria.

-1 -> marco libre.

>0 -> asignado a uno o más procesos. */

atomic_t _mapcount; /* no de Tablas de Páginas referenciando

al marco de memoria. */

union {

struct {

unsigned long private;

struct address_space *mapping;

Page 66: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 64

};

spinlock_t ptl;

};

pgoff_t index;

struct list_head lru;

void *virtual;

};

Los marcos de memoria pueden ser solicitados usando seis diferentes funciones y macros. La funciónalloc_pages() es una de ellas, las cuales generalmente, retornan la dirección lineal de la primer páginaalojada o NULL si el alojamiento falló. El campo _count es muy útil durante este proceso debidoa que el kernel debe saber cuáles páginas están libres y cuáles no. Además, estas estas funcionespueden recibir diferentes parámetros que para controlar la forma en que el pedido de memoria debe serrealizado. Por ejemplo, la bandera __GFP_WAIT especi�ca que el kernel puede bloquear al procesosolicitante de memoria o la bandera __GFP_REPEAT le indica al kernel que debe reintentar todointento de alojamiento hasta que se logre exitosamente.

La función free_pages() es una de cuatro funciones y macros encargadas de liberar marcos dememoria. Cuando esto ocurre, el campo _count del descriptor de la página física se decrementa y sieste llega a −1 simboliza que el marco ahora está libre.

4.13.2. TLB

Además de los caches del hardware, los procesadores 80x86 incluyen otro cache denominado TLB(Translation Lookaside Bu�er) para acelerar el proceso de traducción de direcciones lineales. Cuandouna dirección lineal es usada por primera vez, la dirección física correspondiente es computada a travésde un acceso lento al conjunto de tablas de páginas en la RAM. La dirección física es luego almacenadaen un entrada de la TLB para que futuras referencias a la misma dirección lineal pueda ser rápidamentetraducida.

Como las direcciones lineales son inherentes a cada proceso, durante un cambio de contexto y elregistro cr3 se modi�ca, el hardware invalida todas las entradas de la TLB porque un nuevo conjuntode tablas de páginas se usará y la TLB está apuntando a información vieja.

El kernel provee un grupo de macros para invalidar toda la TLB de un proceso en ejecución o algunaentrada especí�ca. Entre algunas de ellas, de�nidas en include/asm-i386/tlb�ush.h, se encuentran:

�ush_tlb_all : limpia todas las entradas de la TLB.

�ush_tlb_mm: limpia todas las entradas de la TLB de las páginas no globales de un procesodado.

�ush_tlb_page: limpia la TLB para un sola entrada de la Tabla de Páginas.

Siempre que se produzca un cambio en un entrada de la Tabla de Páginas de un proceso, se deberíalimpiar su correspondiente entrada en la TLB para forzar a una nueva traducción lenta de la direcciónlineal. De otro modo, el hardware obtendrá la traducción directamente, la cual, dependiendo el cambioen la entrada original, puede ser errónea.

4.13.3. Memoria Alta

En la arquitectura 80x86, toda memoria física por sobre el límite de 896 MB es memoria alta(HIGHMEM ) y no es mapeada permanente ni automáticamente dentro del espacio de direcciones delkernel, a pesar de que los procesadores x86 son capaces de direccionar hasta 4 GB de RAM física.

Page 67: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 65

Después de ser alojadas (alloc_pages() con la bandera __GFP_HIGHMEM ), estos marcos deben sermapeados dentro del espacio de direcciones del kernel.

Para mapear una estructura page en el espacio de direcciones del kernel se usa la función kmap()de�nida en include/linux/highmem.c. Esta función funciona tanto para memoria baja como alta. Sila memoria es baja, la dirección virtual es retornada. Si el marco reside en memoria alta, un mapeopermanente es creado y la dirección es retornada.

Como el número de mapeos permanentes es limitado (si no, no existiría este problema), los mapeosde memoria alta deben ser liberados cuando no sean más requeridos. Esto se realiza a través de lafunción kunmap().

Por último, cuando un proceso no puede dormir (ej. manejador de interrupción) el kernel proveemapeos temporales de memoria alta. La función kmap_atomic() es usada para crear el mapeo temporaly asegura que el proceso invocador nunca se bloquee al esperar la solicitud. La rutina kunmap_atomic()deshace el mapeo, aunque podría no disponerse de esta función ya que un mapeo temporal dura hastaque el próximo mapeo temporal es solicitado.

4.13.4. Espacio de Direcciones de un Proceso

El espacio de direcciones (address space) de un proceso consiste en todas las direcciones lineales queel proceso puede usar. Cada proceso tiene un conjunto diferente de direcciones lineales; las direccionesusadas por un proceso no tienen ninguna relación con la usadas por otro. El kernel puede dinámicamentemodi�car el espacio de direcciones de un proceso agregando o removiendo intervalos de direccioneslineales.

Los intervalos son representados por medio de recursos denominados regiones de memoria, los cualesson caracterizados por una dirección lineal inicial, una longitud y algunos derechos para el acceso. Lasrazones más comunes por las cuales un proceso puede obtener nuevas regiones de memoria son:

Al crearse un nuevo proceso a través de la llamada fork(). El proceso hijo recibe un nuevo espaciode direcciones y, por lo tanto, un conjunto de regiones de memoria.

Un proceso ejecutando decide correr un programa totalmente diferente. El proceso permaneceigual, pero las regiones de memoria usada anteriormente son liberadas y un nuevo conjunto esasignado al proceso.

Un proceso realiza el mapeo a memoria de un archivo (o porción de éste). El kernel asigna unanueva región de memoria al proceso para mapear al archivo. Ver sección 4.13.4.1.

un proceso continua agregando información a su pila de modo de usuario hasta que toda la regiónque mapea a la pila es usada. En este caso, el kernel puede decidir expander el tamaño de laregión.

Un proceso puede crear un región de memoria compartida IPC (detallada en la sección 4.12.3.3)para compartir información con otros procesos. El kernel asigna una nueva región al proceso paraimplementar este recurso.

Un proceso puede expandir su área dinámica (heap) a través de funciones como malloc().

El manejador de Fallos de Página del kernel debe saber identi�car las regiones de memoria de unproceso dado, ya que debe e�cientemente distinguir entre dos direcciones lineales inválidas causantes desu invocación: las causadas por errores de programación y aquellas causadas por páginas no presentesen memoria. Esto último puede ocurrir porque a pesar de que la dirección lineal pertenece al proceso,el marco de memoria correspondiente todavía no ha sido alojado. Esto último se denomina demandade página (demand paging) y será discutida en la sección 4.13.6.

Page 68: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 66

Toda la información relacionada al espacio de direcciones de un proceso es incluida en el objetodenominado descriptor de memoria del tipo mm_struct. El objeto es referenciado por el campo mmen la task_struct del proceso. La mm_struct está de�nida en include/linux/sched.h.

Linux implementa una región de memoria por medio de un objeto del tipo vm_area_struct, de�nidoen include/linux/mm.h de la siguiente manera:

struct vm_area_struct {

struct mm_struct * vm_mm; /* Espacio de dir al que pertenezco. */

unsigned long vm_start; /* Dirección inicial en la mem virtual. */

unsigned long vm_end; /* Primer byte después del fin de nuestra

última dirección en la mem virtual. */

struct vm_area_struct *vm_next; /* Otras áreas VM en el proceso. */

pgprot_t vm_page_prot; /* Permisos de acceso de esta VMA. */

unsigned long vm_flags; /* Banderas de la región. */

struct rb_node vm_rb;

/* union usada para mapeo reverso */

union {

struct {

struct list_head list;

void *parent;

struct vm_area_struct *head;

} vm_set;

struct raw_prio_tree_node prio_tree_node;

} shared;

struct list_head anon_vma_node;

struct anon_vma *anon_vma; /* Para mapeo reverse en memoria anónima. */

struct vm_operations_struct * vm_ops; /* Puntero a métodos de la región. */

unsigned long vm_pgoff; /* Desplazamiento (dentro de vm_file). */

struct file * vm_file; /* Archivo al que mapeamos (puede ser NULL). */

void * vm_private_data;

unsigned long vm_truncate_count;

};

Cada descriptor identi�ca un intervalo de direcciones lineales. Las regiones de memoria de unproceso jamás se solapan.

El campo vm_ops apunta a la estructura de datos vm_operations_struct, la cual almacena losmétodos de la región de memoria. Solo existen cuatro métodos:

open Invocado cuando la región de memoria es agregada al conjunto de regiones del procesodueño.

close Invocado cuando la región de memoria es removida del conjunto de regiones del procesodueño.

Page 69: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 67

nopage Invocada por el manejador de excepciones de Fallo de Página cuando un procesointenta acceder a una página no presente en RAM cuya dirección lineal pertenece a la región dememoria.

populate Invocada para iniciar las entradas del conjunto de tablas de páginas correspondientesa la direcciones lineales de la región de memoria (antes del fallo).

Para �nalizar aclaremos la relación entre página y región de memoria. Una región de memoriaconsiste en un conjunto de páginas consecutivas.

Ya tratamos dos tipos de banderas asociadas a páginas:

Las banderas almacenadas en cada entrada de la Tabla de Páginas (Present, Read/Write, etc).

Las banderas almacenadas en el campo �ags del descriptor de cada página, page.

El primer tipo es usado por el hardware para chequear si un tipo de direccionamiento puede serrealizado; el segundo es usado por Linux para diferentes propósitos (bloquear la página en memoria,saber pertenece a memoria alta, saber si fue barrida, etc).

Ahora se introduce un tercer tipo de bandera: aquellas asociadas a las páginas de una región dememoria. Estas son almacenadas en el campo vm_�ags de la estructura vm_area_struct. Algunasofrecen información sobre todas las páginas en la región, como qué contiene o que derechos el procesoposee para acceder cada página. Otras banderas describen a la región en sí, como cuando puede crecero si es usada para implementar el recurso IPC de memoria compartida. Todas estas banderas estánde�nidas en include/linux/mm.h.

4.13.4.1. Mapeos en Memoria

Para alojar un nuevo intervalo de direcciones lineales para un proceso, el kernel utiliza la funcióndo_mmap(), de�nida en include/linux/mm.h, quien a su vez invoca a do_mmap_pgo�(). Esta rutinacrea e inicializa una nueva región de memoria para el proceso actual. Sin embargo, hay casos que estono es cierto, ya que si el intervalo es adyacente a un área de memoria virtual que posee los mismospermisos (vm_�ags) que la que se desea crear, los dos intervalos son fusionados en uno.

Una región de memoria puede estar asociada a alguna porción de un archivo regular en un sistemade archivo o un dispositivo de bloque. Esto signi�ca que un acceso a un byte de una página en laregión es traducido por el kernel en una operación en el correspondiente byte del archivo. Esta técnicaes llamada mapeo en memoria.

El prototipo de do_mmap_pgo�() es:

unsigned long do_mmap_pgoff(struct file * file, unsigned long addr,

unsigned long len, unsigned long prot,

unsigned long flags, unsigned long pgoff);

La función mapea el archivo especi�cado por �le con desplazamiento pgo� y para una largo de lenbytes. El parámetro �le puede ser NULL y el desplazamiento 0, en cual caso el mapeo no pertenecerá aun archivo perteneciente a disco. La variable addr especi�ca la dirección lineal donde se quiere mapearel archivo. Está debe ser múltiplo del tamaño de página (PAGE_SIZE ) o puede no ser especi�cadadejando al kernel que elija la dirección más conveniente.

El parámetro �ags se corresponde con las banderas de la estructura vm_area_struct y a partir deellas y el archivo a mapear, podemos clasi�car los mapeos en memoria en las siguientes clases:

Mapeo de Archivo

En este caso se intenta mapear un archivo regular de disco en un intervalo de memoria delproceso. Los mapeos de archivos se realizan a través de la llamada al sistema sys_mmap2().

Page 70: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 68

La bandera MAP_PRIVATE hace que el mapeo sea privado, o sea, que solo el proceso invo-cador de la llamada al sistema pueda leer y/o escribir en la memoria mapeada. Ni siquiera losdescendientes del proceso podrán acceder a ella.

La bandera MAP_SHARED permite que el área mapeada pueda ser compartida por variosprocesos. Cuando el proceso se duplique y se alojen nuevas regiones para el nuevo hijo, la funcióndup_mm() detectará que la región es compartida y alojara la nueva región (y sus tablas depáginas) mapeando los mismos marcos de memoria que la VMA original.

En ambos casos, el mapeo puede mapear a todo el archivo o solo una porción.

Memoria Compartida IPC

Cuando se genera una región de memoria compartida IPC (ver sección 4.12.3.3) se invoca a lafunción do_mmap() con la bandera MAP_SHARED y con un archivo particular creado espe-cialmente para mapear este tipo de regiones. El inodo del archivo es el mismo al que posee elarchivo del campo shm_�le en la estructura shmid_kernel.

Recordemos que este archivo era creado para integrar al recurso de memoria compartida en elVFS. Por lo tanto, este tipo de comunicación IPC no di�ere mucho de un mapeo en memoria.Sin embargo, la diferencia radica en cómo el reclamo de marcos de memoria y la técnica dedemanda de páginas tratan a las páginas pertenecientes a esta región. Cuando tratemos conestos mecanismos más adelante se explicitarán estas diferencias.

Mapeo de Anónimo

En este caso la bandera MAP_ANONYMOUS es especi�cada y el parámetro �le es NULL. Estoimplica que el mapeo es anónimo y que no mapea a ningún archivo en disco. Sin embargo, lanueva región necesita un conjunto de métodos para operar sobre las páginas y un modo de saberquién es su dueño. Para esto usa las rutinas internas del recurso IPC de memoria compartidapara alojar un nuevo objeto �le y asociarlo a la nuevo región.

Gracias a esto, las técnicas de reclamo de marcos y de demanda de páginas realizarán lo mismopara este tipo de regiones como para las regiones de memoria compartida IPC.

Un mapeo anónimo puede ser compartido o no y eso depende de si la bandera MAP_SHAREDfue especi�cada también durante la invocación.

Para liberar los marcos de memoria de las páginas pertenecientes a una región de memoria virtualse utiliza la función unmap_region() invocada por la rutina do_unmap().

4.13.5. Manejador de Excepciones de Fallo de Página

El manejador de Fallos de página debe distinguir entre errores de programación y los fallos causadospor páginas legítimas pero todavía no alojadas. Este es servido por la rutina do_page_fault() de�nidaen arch/i386/mm/fault.c.

Dado un fallo de página, su tarea básicamente consiste en:

¾La dirección pertenece al espacio de direcciones del proceso?

SI ¾Concuerdan los derechos de acceso con los derechos de acceso de la región de memoria?

SI Acceso Legal: alojar un nuevo marco de memoria.

NO Acceso Ilegal: mandar señal SIGSEGV (Segmentation Fault).

NO ¾La excepción ocurrió en modo de usuario?

SI Acceso Ilegal: mandar señal SIGSEGV (Segmentation Fault).

NO Bug del kernel: matar al proceso.

Page 71: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 69

4.13.5.1. Copy On Write

Cuando un llamada al sistema fork() es invocada el kernel debe duplicar el espacio de direccionesdel padre y asignar la copia al hijo. Esta actividad consume mucho tiempo, ya que requiere:

Alojar marcos de memoria para el conjunto de tablas del proceso hijo.

Alojar marcos de memoria para las páginas del hijo.

Inicializar el conjunto de tablas del hijo.

Copiar las páginas del padre en las correspondientes páginas del hijo.

Para realizar esto más e�cientemente, los sistema Unix modernos utilizan una técnica denominadaCopy On Write (copiar en escritura). La idea es diferir la duplicación de cada marco de memoriahasta que ya sea padre o hijo escriban en el marco. Mientras no haya escrituras todo marco puede serleído por cada proceso y no habría ningún problema en ello, ya que la información es la misma paraambos. Las banderas Read/Write de los marcos son desactivadas pero el contador del descriptor pagees incrementado en 1. Cuando alguno de los procesos desea escribir sobre una página se dispara unexcepción y el manejador de Fallos de Páginas puede identi�car a partir del contador que la páginadebe ser duplicada para el nuevo proceso. Después de esto, la bandera Read/Write del nuevo marco esactivada para que la excepción no se dispare más en el futuro.

De esta forma, tanto padre como hijo compartirán las páginas siempre y cuando lean de ellas lo quehace que aquellos marcos que nunca serán escritos por ningún proceso (código del programa, librerías,etc.) no serán duplicados en vano, comprometiendo al desempeño del sistema.

4.13.6. Demanda de Páginas

El término demand paging o demanda de páginas denota una técnica de alojamiento dinámico dememoria que consiste en diferir el alojamiento de marcos de memoria hasta el último momento posiblehasta que el proceso intente direccionar una página no presente en la RAM, causando una excepciónde Fallo de Página.

Durante un cambio de contexto, sería una pérdida de tiempo direccionar a todas las direccioneslineales contenidas en el espacio de direcciones del nuevo proceso, ya que durante su turno quizá noutilice muchas de éstas. Es más existe un principio que asegura que, en cada etapa de la ejecución deun programa, solo un pequeño subconjunto de las páginas del proceso serán referenciadas, dejando quelos marcos que contiene a las temporalmente páginas sin usar puedan ser usados por otros procesos.La demanda de páginas permite al sistema obtener una mejor performance con la misma cantidad deRAM.

El precio de está técnica en la sobrecarga del sistema, cada Fallo de Página inducido por la demandadebe ser manejado por el kernel, gastando ciclos del CPU. Sin embargo, el principio también dicta queuna vez que el proceso empieza a trabajar con un grupo de páginas, se mantiene con éstas sin direccionarotras páginas por un tiempo.

Una página puede no estar presente en memoria porque el proceso nunca la ha accedido antes,o porque el correspondiente marco de memoria fue reclamado por el kernel. De cualquier modo elmanejador de fallos debe asignar un nuevo marco al proceso y la forma en que el marco es inicializadodepende del tipo de la página. No vamos ahondar más en este detalle, pero es bueno saber que comoúltima instancia se termina siempre invocando al método nopage del conjunto de operaciones de�nidaspara la región de memoria a la cual pertenece la página. En caso de que el método no existiese (mapeoanónimo) se llama a la función do_anonymous_page().

Básicamente el método nopage realiza lo siguiente:

Si la página pertenece a un mapeo de un archivo de disco, el contenido del nuevo marco alojadose obtendrá del bloque de disco correspondiente a la página.

Page 72: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 70

Si la página pertenece a una región de memoria anónima o a una región de memoria compartidaIPC, pueden ocurrir dos situaciones:

• Que la página sea usada por primera vez, por lo que, está se inicializará con todos ceros.

• Que la página ya haya sido usada y en tal caso, el contenido del nuevo marco alojado seobtendrá de la entrada en el área de intercambio (swap) que corresponde a la página. Esteúltimo concepto se detallará en la sección 4.13.8.1.

4.13.7. El Cache de Páginas

Linux implementa un cache primario de disco denominado cache de páginas o page cache. Suobjetivo es minimizar las operaciones de E/S del disco al almacenar en memoria física la informaciónque sería accedida desde el disco.

Los caches de disco son bene�ciosos porque el acceso al disco en mucho más lento que el acceso amemoria.

El cache de páginas consiste en un grupo de páginas físicas en RAM. Cada página en el cachecorresponde a múltiples bloques en el disco. Cuando el kernel empieza una operación de E/S, primerochequea si la información solicitada está en el cache, y si lo está, se olvida totalmente del disco y leelos datos directamente desde la memoria.

Las páginas del cache se originan de lecturas y escrituras de archivo regulares de sistemas de archivo,archivos de dispositivos de bloque y archivos mapeados en memoria.

4.13.7.1. El objeto address_space

Una marco de memoria puede comprender múltiples bloques físicos no contiguos. Chequear el cachede páginas para ver si ciertos datos han sido cacheados es muy di�cultoso por la naturaleza no contiguade los bloques que constituyen cada página. Por lo tanto, no es posible indexar datos en el cache usandosolo el nombre del dispositivo y el número de bloque. Además, el cache de páginas apunta a ser genéricoy por ende cachear cualquier objeto basado en páginas.

Para mantener esto, Linux usa la estructura address_space para identi�car a cada página en elcache. Esta estructura está de�nida en include/linux/fs.h:

struct address_space {

struct inode *host; /* dueño: inodo, disp. de bloque */

struct radix_tree_root page_tree; /* árbol de todas las páginas */

rwlock_t tree_lock;

unsigned int i_mmap_writable;/* conteo de mapeos VM_SHARED */

struct prio_tree_root i_mmap; /* árbol de mapeos */

struct list_head i_mmap_nonlinear; /*lista mapeos VM_NONLINEAR */

spinlock_t i_mmap_lock;

unsigned int truncate_count;

unsigned long nrpages; /* no total de páginas */

pgoff_t writeback_index;

const struct address_space_operations *a_ops; /* métodos */

unsigned long flags;

struct backing_dev_info *backing_dev_info;

spinlock_t private_lock;

struct list_head private_list;

struct address_space *assoc_mapping;

}

Page 73: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 71

El campo i_mapping de un inodo siempre está apuntando a su estructura address_space.Un campo crucial de esta estructura es a_ops que apunta a la tabla del tipo address_space_-

operations que contiene los métodos que de�nen cómo las páginas serán tratadas. Algunos de estosmétodos son:

writepage Escribe desde la página a la imagen en disco.

readpage Lee desde la imagen en disco a la página.

sync_page Empieza transferencias de E/S de datos en las páginas.

prepare_write Prepara una operación de escritura.

commit_write Completa una operación de escritura.

Por último vale mencionar algunas de las funciones que utiliza el cache de páginas para aplicar sufuncionalidad:

�nd_get_page()

Encuentra y toma una referencia a una página. Recibe un puntero a address_space y el desplaza-miento del sector del archivo para el cual se quiere obtener la página.

�nd_lock_page()

Idem anterior, pero además incrementa el conteo de uso de la página retornada y activa la banderaPG_LOCKED en el descriptor page del marco de memoria al que apunta la página obtenida. Lapágina podrá ser accedida exclusivamente por el proceso invocador. La rutina lock_page() querealiza esto puede dormir al proceso en caso de que la página ya se encuentre bloqueada, paraeso utiliza a la función __wait_on_bit_lock(). unlock_page() desbloquea la página.

add_to_cache_page()

Inserta un nuevo descriptor de página al cache de páginas.

remove_from_cache_page()

Remueve un descriptor de página del cache de páginas.

read_cache_page()

Se asegura que el cache incluya una versión actualizada de una cierta página. Para invocar a estafunción, además del objeto address_space y el indice de la página, se debe proveer una funciónque sea capaz de leer la información de la página desde el disco. Normalmente esta rutina, si estáde�nida para la página, es el método readpage().

4.13.8. Reclamo de Páginas

El kernel de Linux hace uso la memoria RAM disponible de la mejor manera posible. Cuando lacarga del sistema es baja, la RAM está ocupada por las caches de disco y los pocos procesos corriendose pueden bene�ciar de la información almacenada en ellos. Sin embargo, cuando la carga del sistemaes alta, la RAM está ocupada en su mayoría por páginas de procesos y las caches son escogidas parahacer espacio a procesos adicionales.

Procesos y caches toman y toman cada vez más páginas físicas de memoria pero nunca liberanningunas de éstas. Tarde o temprano toda la memoria libre será asignada a procesos y caches. Elmecanismo de reclamo de marcos o page frame reclaiming rellena la lista de bloques libres del kernelal �robar� marcos de memoria de los procesos de espacio de usuario y las caches del kernel.

Page 74: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 72

El algoritmo que usa el kernel para realizar esta tarea debe tomar marcos de memoria (obviamente,no libres) y hacerlos libres. Los marcos de memoria a liberar son manejados dependiendo el tipo deinformación que almacenan. Estos tipos son:

Páginas No Reclamables

Estas páginas físicas no puede ser reclamadas por el algoritmo. Normalmente son:

Marcos libres.

Marcos de memoria reservados (con la bandera activa PG_reserved en su descriptor page).

Marcos dinámicamente alojados por el kernel.

Marcos pertenecientes a la Kernel Mode Stack de los procesos.

Marcos temporalmente bloqueados (con la bandera activa PG_locked en su descriptor page).

Marcos bloqueados de una región de memoria (bandera VM_LOCKED en el descriptorvm_area_struct).

Páginas Intercambiables

Estos marcos de memoria pertenecen a páginas anónimas en los espacios de direcciones de proce-sos de usuario (ej. heap o la pila de modo usuario). También bajo esta categoría se encuentran lasmarcos mapeados a archivos de sistemas de archivos temporales (ej. Memoria compartida IPC,ver sección 4.13.4.1).

Como se verá en la sección 4.13.8.1 el contenido de los marcos reclamados se almacenará en unárea especial llamada swap.

Páginas Sincronizables

Bajo esta categoría se encuentran los marcos asociados a páginas de archivo de disco mapeadosen memoria, páginas de la cache de páginas que contiene datos de archivos de disco, páginas debu�ers de dispositivos de bloque y páginas de algunas caches de disco (ej. el cache de inodos).

Si el contenido de la página esta marcado como sucio dirty, entonces se sincronizará la páginacon su respectiva imagen en disco.

Páginas Descartables

Estas son los marcos de páginas no usadas en las cache de memoria o la dentry cache, conceptoque se introducirá más adelante en la sección 4.14.3.

Al reclamar el marco nada se lleva a cabo ya que estos no contiene información útil.

El algoritmo de reclamo se basa en ciertas heurísticas para realizar su función. Particularmente setiene mucho en cuenta la jerarquía de las caches, la edad de las páginas y si éstas están sucias (banderaAccessed y Dirty, respectivamente, en la entrada de la Tabla de Páginas).

4.13.8.1. Swapping

El término swapping (intercambio) es usado describir al método que utiliza el kernel para ofrecerun respaldo de seguridad para las páginas no mapeadas a archivos.

A esta altura ya sabemos que hay tres tipos de páginas:

Páginas pertenecientes a regiones de memoria anónimas.

Páginas pertenecientes a un archivo de disco mapeado en memoria.

Páginas pertenecientes a una región de memoria compartida IPC.

Page 75: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 73

Cuando el algoritmo de reclamo selecciona un marco de memoria en particular, la página asociada(el contenido del marco) debe ser almacenado en algún lugar especial. Esto se debe a que la páginamuy probablemente sea utilizada nuevamente en el futuro y el nuevo marco alojado para ella debecontener la misma información que poseía su marco de memoria anterior.

Cuando la página pertenece a un mapeo de un archivo de disco la copia de respaldo se realiza actu-alizando el bloque de disco con el contenido de su página asociada en el mapeo. Como ya mencionamos,esta técnica se denomina sincronización (synching).

Sin embargo, las páginas pertenecientes a las otras dos categorías no mapean un archivo ubicadoen un dispositivo de bloque y por ende, no pueden ser sincronizadas. Para subsanar esta restricciónpáginas existe el área swap o de intercambio, la cual puede ser implementada como una partición deldisco rígido o como un archivo incluido en una partición grande.

Puede haber múltiples áreas de intercambio y cada una consiste en una secuencia de bloques de4.096 bytes para contener a la página barrida de la memoria física.

4.13.8.2. Mapeo En Reversa

Cuando se remueve un marco de memoria perteneciente a un proceso, el contexto de éste debeser noti�cado de tal evento. Para esto toda entrada de la Tabla de Páginas que referencia el marcoreclamado es limpiada para que en el futuro, cuando el proceso vuelva a utilizar la página virtual,se produzca un Fallo de Página y se vuelva a alojar un nuevo marco de memoria para contener suinformación. Además todas la entradas de la TLB deberían ser borradas para la página virtual paraque el hardware no realice la traducción directamente sin consultar al kernel.

Un problema surge cuando el marco de memoria es referenciado por varios procesos, ya quepertenece a una región de memoria compartida (VM_SHARED). El kernel debe e�cientemente recor-rer todas las Tablas de Páginas de estos procesos, empezando desde el marco de memoria a reclamar.Esta actividad es llamada mapeo en reversa o reverse mapping.

En primera instancia el algoritmo de reclamo de página debe determinar si el marco es compartidoo no, y si es mapeado o anónimo. Para esto los campos _mapcout y mapping de descriptor page de lapágina son de gran utilidad:

El campo _mapcount contiene la cantidad de Tablas de Páginas que están referenciando al marco:Si el valor es −1, nadie referencia al marco. Si el valor es 0 el marco no es compartido mientrasque si es mayor a 0 signi�ca que lo es.

El campo mapping determina si el marco pertenece a una página mapeada o anónima.

Las funciones try_to_unmap_anon() y try_to_unmap_�le() son las rutinas encargadas de limpiarpáginas anónimas y mapeadas, respectivamente. No describiremos que realizan en detalles estas fun-ciones, pero a continuación describiremos resumidamente como obtienen la Tabla de Páginas de cadaproceso involucrado en el reclamo del marco:

Páginas Anónimas

Las páginas pertenecientes a regiones de memoria anónimas serán compartidas entre diferentesprocesos, pero tales procesos serán descendientes del primer alojador de la región.

La estrategia para agrupar a todas las páginas anónimas que referencian el mismo marco consisteen enlazarlas en una lista del kernel.

Cuando el kernel asigna el primer marco de memoria para una región anónima crea una nuevaestructura de tipo anon_vma:

struct anon_vma {

spinlock_t lock; /* Acceso serializado a la lista de vmas */

Page 76: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 74

struct list_head head; /* Lista de vmas relacionadas */

};

Luego el kernel inserta la estructura vm_area_struct de la región en la lista head. Para este�n, vm_area_struct posee dos campos relacionados a la lista: anon_vma_node que almacenalos punteros al próximo y anterior elemento de la lista, mientras que anon_vma apunta a laestrutura anon_vma. Finalmente, el kernel almacena la dirección de anon_vma en el campomapping del descriptor de la página anónima.

Cuando un marco de memoria referenciado por la Tabla de Páginas de un proceso es insertado enla Tabla de Páginas de otro proceso, el kernel inserta la región de memoria anónima del segundoen la lista doblemente enlazada de la estructura anon_vma apuntada por el campo anon_vmade la región de memoria del primer proceso.

Notar que gracias a la estructura anon_vma referencia por el campo mapping del descriptor delmarco de memoria, el kernel puede obtener a todas las Tablas de Páginas refereciando al marcoa partir de él.

Páginas Mapeadas

La regiones de memoria anónima no son normalmente compartidas y de serlo, su número no esmuy grande por lo que el enfoque de la lista doblemente enlazada para agruparlas es bastantee�ciente. Sin embargo, las regiones mapeadas son muy frecuentemente compartidas porque mu-chos procesos diferentes puede usar las mismas páginas de código (ej. Librería estándar C). Poresta razón, Linux 2.6 se apoya en un tipo especial de árbol de búsqueda denominado árbol debúsqueda por prioridad10 (priority search tree), para localizar rápidamente a todas las regionesde memoria que referencian a la misma página física.

Existe un árbol de prioridad en cada archivo y su raíz es almacenada en el campo i_mmap delobjeto address_space embebido en el objeto inodo del archivo. Es fácil retornar de manera rápidala raíz de un árbol de prioridad, ya que el campo mapping del descriptor de un marco asociadaa una página mapeada apunta al objeto address_space.

Cuando un proceso se duplica y hereda la región mepeada compartida a su hijo o cuandose mapea un archivo a memoria (ver sección mmap), las funciones respectivas dup_mm() ydo_mmap_pgo�() agregan la nueva región de memoria compartida del proceso al árbol debúsqueda por prioridad del archivo mapeado. Para esto se utiliza la función vma_prio_tree_insert().

Cuando un proceso muere o quita su mapeo de memoria a un archivo, la función vma_prio_-tree_remove() es invocada para quitar la región de memoria del proceso del árbol de búsquedapor prioridad.

4.14. Kernel Internals

4.14.1. Listas del Kernel

Una lista del kernel es una estructura de datos que permite el almacenamiento y manipulaciónde un número variables de nodo de información. A diferencia de un arreglo estático, los elementosde una lista enlazada son creados dinámicamente. Esto permite la creación de un número variable deelementos que son desconocidos en tiempo de compilación. Como son creados en diferentes momentos,no es necesario que ocupen espacios contiguos de memoria. De esta forma, los elementos deben estarenlazados juntos, entonces cada elemento contiene un puntero al siguiente elemento.

Una estructura de datos simples que represente una lista enlazada puede ser la siguiente:

10Para ver como funciona esta estructura de datos ver [14]

Page 77: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 75

struct list_element {

void *data; /* La información. */

struct list_element *next; /* Puntero al siguiente elemento. */

};

El kernel de Linux tiene un enfoque único e innovador para recorrer las listas enlazadas. Al recor-rerlas, a menos que el orden sea importante, no importa si uno empieza desde la cabecera de la lista, dehecho, no importa desde donde uno comience en absoluto. Lo importante es visitar a todos los nodos.Para recorrer la lista, simplemente hace falta tomar el elemento y seguir los punteros a los siguiente,hasta llegar al primer nodo visitado. Esto remueve la necesidad de un nodo cabecera especial. Además,las rutinas para manipular una lista enlazada son simpli�cadas. Cada rutina, simplemente, necesita unpuntero a un elemento de la lista: cualquier elemento.

El kernel implementa las lista enlazadas como doblemente enlazadas (referencia el nodo siguiente yel anterior) y circulares. Este tiene un versión o�cial implementada y todos los desarrolladores debenusar su interfaz.

El código está declarado en linux/include/list.h y la estructura de datos es simple:

struct list_head {

struct list_head *next;

struct list_head *prev;

};

Una list_head por si sola es inservible, normalmente, está embebida en una estructura propia:

struct my_struct {

struct list_head list;

int p;

void *n;

};

La lista debe ser inicializada para poder ser usada con:

struct my_struct *mine = {

.list = LIST_HEAD_INIT(mine.list),

.p = 0,

.n = NULL,

};

o

struct my_struct *m;

/* Alojar estructura en memoria ...*/

m->p = 0;

m->n = NULL;

INIT_LIST_HEAD(&p->list);

Uno no tiene por qué referenciar los campos internos de la lista. En cambio, solo hay que incorporarlaen la estructura propia de uno y usar la interfaz de listas enlazadas para manipularlas y recorrerlas.

Page 78: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 76

4.14.2. VFS

Una de las clave del éxito de Linux es su habilidad para coexistir cómodamente con otros sistemas.Uno puede transparentemente montar discos o particiones que posean formatos de archivos usados porWindows, otros sistemas Unix u otro tipo de sistemas. Linux posee la habilidad de manejar múltiplestipos sistemas de archivos de la misma forma que lo hace otras variantes de Unix, a través del conceptodenominado Sistema de Archivos Virtual (Virtual Filesystem).

La idea detrás del Sistema de Archivos Virtual es poner un amplio rango de información en elkernel para representar diferentes tipos de sistemas de archivos; hay un campo o función para queLinux soporte cada operación provista por todos los sistemas de archivos reales. Para cada read, write,u otra función invocada, el kernel substituye la función actual que soporte al sistema de archivos encuestión.

El VFS (Virtual Filesystem Switch) es el subsistema del kernel que implementa la interfaz rela-cionada con sistemas de archivos provista a los programas de usuario. Todos los sistemas de archivos seapoyan en el VFS no solo para coexistir, sino también para interoperar. Esto permite a los programasusar llamadas al sistema estándares de Unix para leer y escribir en diferentes sistemas de archivos endiferentes medios (disquette, cd-rom, disco duro, etc).

Para lograr esto último el VFS introduce un modelo común de archivos (common �le model) capazde representar a todos los sistemas de archivos. Este modelo estrictamente imita el modelo de archivosprovisto por el sistema de archivo tradicional de Unix. Esto no es sorprendente, ya que Linux quierecorrer su sistema de archivos nativo con la mínima sobrecarga. Sin embargo, cada implementaciónespecí�ca de un sistema de archivos debe traducir su organización física en el modelo común de archivosdel VFS.

Por ejemplo, en el modelo común de archivos, cada directorio es visto como un archivo, que contieneuna lista de archivos y otros directorios. Sin embargo, muchos sistemas no Unix usan una Tabla deAlojamiento de Archivos (File Allocation Table o FAT ), que almacena la posición de cada archivo enel árbol de directorios. En estos sistemas, los directorios no son archivos. Para apegarse al modelo delVFS, la implementaciones para Linux de FAT deben ser capaces de construir sobre la marcha, cuandose necesiten, archivos que correspondan a directorios. Tales archivos existirán solo como objetos en lamemoria del kernel.

Esencialmente, el kernel de Linux no puede imponer una función particular para manejar opera-ciones como read() o ioctl(). En cambio, debe usar un puntero por cada operación; el puntero es creadopara apuntar a la función correcta para que el sistema de archivos particular sea accedido. Como ejem-plo, la llamada al sistema sys_read() realiza primero varios chequeos con respecto al archivo que debeleer, la memoria en donde almacenar la lectura, etc. El archivo a leer es representado por la estructurainterna del kernel �le. Esta estructura contiene un campo llamado f_op que contiene puntero a lasfunciones especi�cas del sistema de archivos al que pertenezca. Cuando sys_read() tenga que realizarconcretamente la lectura, tendrá que invocar a la función especí�ca para realizar esto. Por ende, lallamada read() es convertida en una invocación indirecta a otra función:

file->f_op->read(...);

Uno puede pensar al modelo común de archivos como orientado a objetos. Notar también lo parecidode esta forma de invocación con la del framework LSM introducido en la sección 4.4, donde hay unainterfaz común (el kernel en sí) para todo módulo de seguridad y cada hook aplica sus políticas demanera de imponer el modelo de seguridad especí�co.

Para �nalizar, el modelo común de archivos consiste en los siguiente tipos de objetos:

Superbloque: contiene información concerniente al sistema de archivos montado.

Inodo: almacena información general que concierne a un archivo especí�co.

Page 79: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 77

File: almacena información sobre la interacción entre un archivo abierto y un proceso. Estainformación existe solo en la memoria del kernel mientras el proceso conserve el archivo abierto.

Dentry : almacena información sobre el enlace entre un directorio con el respectivo archivo quelo representa. La siguiente sección aborda más sobre este objeto.

4.14.3. El objeto Dentry

Como vimos en la sección anterior, el VFS trata a los directorios como archivos. En la ruta, /bin/vi,ambos bin y vi son archivos, el primero siendo un directorio especial y el segundo un archivo regular.Un inodo representa a ambos componentes. A pesar de esta uni�cación útil, el VFS necesita, frecuente-mente, realizar tareas especí�cas de directorios, como el recorrido del camino(path name lookup) vistoen la sección 4.7. Para facilitar esto, el VFS emplea el concepto de entrada de directorio o dentry.Un dentry es un componentes especí�co en la ruta. Con el anterior ejemplo, /, bin y vi son objetosdentry. Los primeros dos son directorios y el último es un archivo regular. Este es un punto importante:los objetos dentry son todos los componentes de la ruta, incluyendo los archivos. Resolver una rutay caminar a través de sus componentes es un ejercicio no trivial, que consume tiempo y con muchascomparaciones entre cadenas de caracteres. El dentry hace a este proceso mucho más simple.

struct dentry {

atomic_t d_count;

unsigned int d_flags; /* protected by d_lock */

spinlock_t d_lock; /* per dentry lock */

struct inode *d_inode; /* Where the name belongs to - NULL is

* negative */

struct hlist_node d_hash; /* lookup hash list */

struct dentry *d_parent; /* parent directory */

struct qstr d_name;

struct list_head d_lru; /* LRU list */

union {

struct list_head d_child; /* child of parent list */

struct rcu_head d_rcu;

} d_u;

struct list_head d_subdirs; /* our children */

struct list_head d_alias; /* inode alias list */

unsigned long d_time; /* used by d_revalidate */

struct dentry_operations *d_op;

struct super_block *d_sb; /* The root of the dentry tree */

void *d_fsdata; /* fs-specific data */

struct dcookie_struct *d_cookie; /* cookie, if any */

int d_mounted;

unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */

};

Este objeto no se corresponde con ningún tipo de estructura de datos del disco. El VFS lo crea enel momento y como no son almacenados en el rígido, ninguna bandera en la estructura dentry indica siel objeto ha sido modi�cado. Un dentry válido puede tener tres estados: usado, no usado, o negativo.

Un dentry usado corresponde a un inodo válido (d_inode apunta a su inodo asociado) e indicaque hay uno o más usuarios del objeto (d_count es positivo). Al ser usado por el VFS, apunta a dataválida y no puede ser descartado.

Page 80: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 78

Un dentry no válido corresponde con un inodo válido, pero indica que VFS no está actualmenteusándolo (d_count es cero). Como todavía apunta a data válida, el objeto es mantenido por si llega aser utilizado nuevamente.

Un dentry negativo no está asociado a un inodo válido (d_inode es NULL), porque o el inodofue borrado o el nombre de la ruta nunca fue correcto. El objeto se mantiene, a pesar de esto, paraque otros recorridos de caminos futuros sean resueltos rápidamente. Sin embargo, puede llegar a serdestruido en caso de que haga falta memoria.

4.14.3.1. Dentry Cache

Después de que el kernel realice todo el trabajo pesado de resolver una ruta y crear cada dentry,sería una pérdida de tiempo y performance tirar todo el trabajo a la basura. En cambio, el kernelalmacena a los objetos dentry en el dentry cache o, simplemente, la dcache. Para más informaciónsobre este componente ver [16].

4.14.4. Bottom Halves

Algunas veces es razonable separar la cantidad de trabajo que es realizado dentro de un mane-jados de interrupciones en: trabajo inmediato (por ejemplo, reconocer la interrupción, actualizar lasestadísticas, etc.) y trabajo que puede ser pospuesto para más adelante, cuando las interrupcionesestén activadas (por ejemplo, realizar procesamiento posterior de la data, despertar procesos para estainformación, etc.).

Las bottom halves son el mecanismo más viejo para diferir la ejecución de tareas del kernel y estándisponibles desde el Linux 1.x. En el Linux 2.0, un nuevo mecanismo fue adherido, las task_queues, yen el kernel 2.6, se agregan las colas de trabajo, tratadas en la sección 4.11.

Pero, ¾cuándo las bottom halves son ejecutadas? En dos lugares:

En cada schedule().

En cada camino de retorno de una interrupción o una llamada al sistema en entry.S.

4.14.5. Proceso Current

Los procesos son entidades dinámicas cuya vida puede durar entre milisegundos o meses. Por lotanto, el kernel debe ser capaz de manejar muchos procesos al mismo tiempo, y los descriptores de éstosson almacenados en memoria dinámica en vez de en algún sector permanente en memoria asignado alkernel. Para cada proceso, Linux empaqueta dos estructuras de datos diferentes en un área de memoria:una estructura pequeña enlazada al descriptor del proceso (task_struct) llamada thread_info, y unapila para que el proceso utilice en el modo kernel llamada Kernel Mode Stack. La longitud de éste areaes usualmente 8.192 bytes (dos páginas de memoria). Por razones de e�ciencia, el kernel almacena áreasde 8 KB en páginas consecutivas con la primera alineada con un múltiplo de 213; esto puede llegar aser un problema cuando hay poca memoria dinámica disponible, por lo que en la arquitectura 80x86el kernel pueda ser con�gurado en tiempo de compilación para que la pila y la thread_info ocupe unasola página de memoria (4.096 bytes).

struct thread_info {

struct task_struct *task; /* main task structure */

struct exec_domain *exec_domain; /* execution domain */

unsigned long flags; /* low level flags */

unsigned long status; /* thread-synchronous flags */

__u32 cpu; /* current CPU */

int preempt_count; /* 0 => preemptable, <0 => BUG */

Page 81: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 79

mm_segment_t addr_limit; /* thread address space:

0-0xBFFFFFFF for user-thead

0-0xFFFFFFFF for kernel-thread

*/

void *sysenter_return;

struct restart_block restart_block;

unsigned long previous_esp; /* ESP of the previous stack in case

of nested (IRQ) stacks

*/

__u8 supervisor_stack[0];

};

Un proceso en modo kernel accede a una pila contenida en el segmento de datos del kernel, y esdiferente de aquella usado en Modo de Usuario. Como los caminos de control del kernel usan poco estapila, solo unos pocos miles de bytes son requeridos para ella. Por ende, 8 KB es un espacio amplio parala pila y la thread_info.

La �gura 4.3 muestra como la estructura thread_info reside al principio del área de memoria, y lapila crece hacia abajo desde el �n. Las estructuras thread_info y task_struct están ligadas a través delos campos task y thread_info respectivamente.

Figura 4.3: Dos páginas para almacenar al kernel stack y la thread_info.

El registro esp es el puntero de pila del CPU, el cual es utilizado para referencia la siguiente posiciónlibre de la pila. Su valor es decrementado tan pronto como nueva data sea escrita.

En C se podría representar este conjunto como:

union thread_union {

struct thread_info thread_info;

unsigned long stack[2048]; /* 1024 para stacks de 4KB */

Page 82: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 80

};

4.14.5.1. Current

La relación tan cercana entre la thread_info y la Kernel Mode Stack ofrece un bene�cio clave entérminos de e�ciencia: el kernel pueda obtener fácilmente la dirección de la estructura thread_info deun proceso actualmente corriendo en el CPU, a partir del valor de registro esp. De hecho, si la estructurathread_union es de 8KB (213 bytes), el kernel puede maskear los 13 bits menos signi�cativos de esppara obtener la dirección base de thread_info. Esto es realizado por la función current_thread_info()que produce lo siguiente:

movl $0xffffe000, %ecx

andl %esp, %ecx

movl %ecx, p

Después de ejecutar estas tres instrucciones de assembler, p contendrá la estructura thread_info delproceso que actualmente está corriendo en el CPU que ejecuta tal instrucción.

La mayoría de la veces el kernel necesita el descriptor del proceso. Para obtener la task_struct delproceso actual usando el procesador, se usa la macro current, la cual, esencialmente, es equivalentecurrent_thread_info->task y produce el siguiente código de assembler:

movl $0xffffe000, %ecx

andl %esp, %ecx

movl (%ecx), p

Porque el campo task está en el o�set 0 de la thread_info.

4.14.6. Cambios de Contexto

Para controlar la ejecución de procesos, el kernel debe ser capaz de suspender la ejecución de unproceso corriendo en el CPU y resumir la de otro proceso suspendido previamente. Esta actividad llevael nombre de process switch, task switch o context switch (cambio de contexto).

Mientras que cada proceso puede tener su propio espacio de direcciones, todos los proceso delsistema debe compartir los registros del CPU. Por lo tanto, antes de resumir la ejecución de un proceso,el kernel debe asegurarse que tales registros sean cargados con los valores que tenían cuando el procesofue suspendido.

El conjunto de información que debe ser cargada en los registros en mencionada situación es de-nominado el hardware context (contexto del hardware). En Linux, parte de este contexto es guardadoen la task_struct y el resto queda almacenado en la Kernel Mode Stack.

Cada proceso incluye un campo en su descriptor llamado thread de tipo thread_stuct, en donde elkernel salvará el contexto de hardware cuando el proceso sea cambiado. Esta estructura incluye campospara guardar a todos los registros, salvo a aquellos de propósito general como eax, ebx, etc., que sonalmacenados en la pila del kernel.

Un cambio de contexto puede ocurrir en un punto bien de�nido: la función schedule. Esencialmente,cada cambio consiste en dos pasos:

1. Cambiar el Directorio Global de Páginas para instalar el nuevo espacio de memoria.

2. Cambiar la Kernel Mode Stack y el contexto del hardware.

A partir de ahora asumiremos que prev apuntará al proceso a ser reemplazado y next a aquel a seractivado.

Page 83: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 81

4.14.6.1. La macro switch_to

El segundo paso de los descriptos arriba es realizado por la macro switch_to. Es una de las rutinasmás dependientes del hardware del kernel, y conlleva cierta di�cultad entender qué hace.

La macro recibe cuatro parámetros prev, next y last. Ya sabemos que son los dos primeros. Eltercero, last, apunta al tercer proceso involucrado en el cambio de contexto. Pero, ¾cómo esto esposible? Supongamos que el kernel decide sacar A para activar a B. En la función schedule(), prevapunta al descriptor de A y next apunta al de B. Tan pronto como switch_to desactiva A, el �ujo deejecución de este proceso se congela.

Luego, cuando el kernel quiera reactivar a A, debe sacar a otro proceso C (en general, diferente deB) ejecutando switch_to con prev apuntado a C y next a A. Cuando A resuma su �ujo de ejecución,encontrará su vieja pila de modo kernel, entonces la variable prev apuntará a A y next a B. El scheduler,quien ahora ejecuta en nombre de A, ha perdido la referencia a C. Esta referencia será útil paracompletar el cambio de contexto. Esto no se discutirá aquí ni tampoco cómo se guarda en cada cambiola referencia a last, pero la idea de la discusión fue hacer conocer la necesidad de tres procesos pararealizar un cambio de contexto.

La macro switch_to está codi�cada en extended inline assembly language11 que hace su lectura untanto compleja. Se describe como, analógicamente, estaría programada en assembler puro, para unamejor comprensión:

Salva los valores de prev y next en los registros eax y edx, respectivamente:

movl prev, %eax

movl next, %edx

Guarda los contenidos de los registros e�ags y ebp en la pila de modo kernel de prev. Deben sersalvados porque el compilador asume que permanecerán igual hasta el �nal de switch_to:

pushfl

push %ebp

Guarda el contenido de esp en prev->thread.esp para que el campo apunte al tope de la pila deprev12:

movl %esp, 484(%eax)

Carga next->thread.esp en esp. A partir de ahora, el kernel opera en la pila de modo kernel denext, por lo que esta instrucción realiza, de hecho, el cambio de contexto de prev a next. Como, eldescriptor del proceso está muy ligado con la pila (ver 4.14.5), cambiar las stack signi�ca cambiarel proceso current.

movl 484(%edx), %esp

Salva la dirección etiquetada 1 en prev->thread.eip. Cuando el proceso reemplaza resuma suejecución, este ejecutará la instrucción etiquetada como 1.

movl $1f, 480(%eax)

En la stack del kernel de next, la macro pone el valor de next->thread.eip, el cual, en la mayoríade los casos es la dirección etiquetada como 1.

pushl 480(%edx)

11El inline assembler es una característica de algunos compiladores que permite escribir código de muy bajo nivel enassembler para ser embebido en un lenguaje de alto nivel como C

12El operando 484(%eax) identi�ca la celda de memoria cuya dirección es el contenido de eax más 484.

Page 84: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 4. CONCEPTOS AVANZADOS DEL SISTEMA OPERATIVO LINUX 82

Salta a la función __switch_to() en C, la cual hace trabajo extra, guardando los demás registrosde prev, cargando los correspondientes a next y resumiendo la ejecución de este último con larutina ret, la cuál usa el valor de eip almacenado en el tope de la pila.

jmp __switch_to

Aquí supongamos que A, el cual fue reemplazado por B toma de vuelta el CPU: ejecuta unapocas instrucciones que restauran los contenidos de los registros e�ags y ebp. La primera de estasdos instrucciones es etiquetada como 1. Como puede verse, 1 es la instrucción inmediatamentesiguiente a jmp __switch_to, por lo que cuando el proceso quitado regrese a tomar el CPU,seguirá como si nada hubiese pasado. O sea, terminará la macro switch_to, luego la funcióncontext_switch() que invocó a la macro y para �nalizar schedule(). Por esa razón al eip de prevse le asigna 1.

1:

popl %ebp

popfl

Notar como estas instrucciones pop se re�eren a la pila del proceso prev. Estas serán ejecutadascuando el scheduler seleccione prev como el nuevo proceso para que corra en el CPU, invocandoa switch_to con prev como el segundo parámetro. Por lo tanto, el registro esp apunta a la pilade prev.

Copia el contenido del registro eax (cargado en el primer paso) en la ubicación de memoriaidenti�cada por el tercer parámetro last.

movl %eax, last

El registro eax apunta al descriptor de proceso de aquel que fue recientemente reemplazado.

Page 85: Flowx: Implementación de no interferencia en Linux

Capítulo 5

Lectura y Escritura de archivos

A partir de este capítulo se detallará por completo la implementación del modelo de seguridad Flowxde no interferencia. La idea es desarrollar diferentes prototipos simples para comprobar el correctofuncionamiento de las políticas de seguridad impuestas. La implementación se hará sobre un módulode seguridad usando el framework LSM (Linux Security Modules). Para mayor información sobre estatecnología consultar la sección 4.4.

En primera instancia, se trabajará con un Linux versión 2.6.21.3 funcionando en consola de texto(sin sistema de ventanas X). Trabajos futuros proponen extender esta implementación para tener encuenta esto último.

5.1. Los Requerimientos

El requisito para el primer prototipo es implementar la no-interferencia solo sobre la lectura yescritura en archivos del sistema de archivos ext3. Este primer prototipo debe ser relativamente simplepara poder testear la columna vertebral de todo el modelo y, así, marcar un camino a seguir.

El eje para poder controlar el acceso a la información se encuentra en asignar a los archivos clasesde seguridad1 que re�ejen la sensibilidad de los datos que contienen. Además, cada sujeto dentro delsistema también debe poseer dicha clase de seguridad para identi�car el tipo de información que puedeacceder. Flowx llamará sujetos a los procesos del sistema, y objetos a los archivos en disco. Cada unode estos componentes tendrá asociada una clase de acceso que consistirá de un nivel de seguridad juntocon un conjunto de categorías. Entonces una clase de seguridad sc se de�ne como par ordenado (n, c)donde n es un nivel y c un conjunto de categorías. Sean sc1 = (n1, c1) y sc2 = (n2, c2) dos clases deacceso, entonces sc1 � sc2 ⇔ n1 ≤ n2 ∧ c1 ⊆ c2.

Las reglas para controlar el �ujo de información en este prototipo corresponden a las reglas de SMdenominadas SM − read(il), SM − read(ih), SM − write(ol) y SM − write(oh) aplicadas únicamentea los archivos regulares de ext3. Según la especi�cación SM debe existir una memoria por cada nivelde seguridad con el cual trabaje un programa. Además, SM debe correr ese programa sobre cadamemoria. Como ya mencionamos en la sección 3.5.4, la estrategia general de implementación es dividiro �forquear� un proceso a medida que requiere acceder a archivos con una clase de acceso diferente de laque accedía hasta ese momento. De esta forma cada proceso creado por esta vía tendrá en su memoriadatos hasta cierto nivel de sensibilidad. Por otro lado, la especi�cación de correr el programa sobrecada memoria se logra trivialmente pues cada proceso así creado ejecuta el mismo código únicamentesobre su propio espacio de memoria.

Si bien esta estrategia resuelve las cuestiones más importantes, quedan por especi�car algunospuntos sobre cómo y cuándo dividir a cada proceso, cuál puede leer de un archivo dado, cuál puede

1Clase de seguridad o clase de acceso serán sinónimos para nosotros y se usarán indiferentemente a lo largo delcapítulo.

83

Page 86: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 84

escribir, etc. En resumen, debemos re�nar la especi�cación SM .Si P es un proceso, llamaremos s-hermano de P a cada proceso que sea creado a raíz de que P o

cualquiera de sus s-hermanos solicita acceso a información con una clase de acceso diferente a la queaccedía hasta ese momento. Un s-hermano de un proceso P es otro proceso exactamente igual a Pen todo aspecto (archivos abiertos, contenidos de registros, program counter actual, etc.). Sea P unproceso del sistema con clase cP

2, sea A un archivo con clase cA, entonces:

Lectura (es decir reglas SM − read(il) y SM − read(ih)):

P puede leer A sii cA � cP .

En caso de que cP ≺ cA, veri�car que P no tenga un s-hermano con la misma clase cA.

• Si existe tal s-hermano retornar como si el pedido de lectura hubiese sido exitoso perodevolviendo basura en el bu�er otorgado por P .

• Si no existe un s-hermano, crear un uno con clase de acceso cA y retornar del pedidocomo se detalló en el item anterior.

Escritura (es decir reglas SM − write(ol) y SM − write(oh)):

Puede escribir en A únicamente el s-hermano cuya clase de acceso sea igual a cA. Si noexiste un s-hermano con tal clase de acceso, entonces escribirá aquel cuya clase de accesoes sup {sc: clase de acceso | sc es la clase de acceso de un s-hermano con sc � cA}

Entonces un aspecto importante es el de añadir las clases de seguridad a nuestros archivos y a losdiferentes procesos en nuestro sistema. En Linux, cada proceso esta identi�cado en el kernel a travésde una estructura llamada task_struct. Esta estructura tiene una instancia única por cada procesoexistente, lo cual la torna ideal para contener la clase de seguridad especí�ca del proceso. Agregar unaclase de acceso a un archivo es un tanto más complicado.

Todo archivo está almacenado físicamente en algún tipo de dispositivo de almacenamiento. Estepuede ser un disco duro, un cd-rom, un disquette, etc. Cada dispositivo tiene su forma de acceder a losarchivos almacenados en él, tanto sea para crear, leer o escribir alguno de estos �cheros. Sin embargo,Linux provee una capa más arriba que aquella en donde residen los controladores especí�cos de cadauno de estos dispositivos, llamada Virtual Filesystem Switch o VFS. Esta capa maneja todas lasllamadas al sistema relacionadas con un sistema de archivos estándar de Unix y su principal fuerza esproveer una interfaz común para diferentes tipos de sistemas de archivos3.

Cada archivo en el VFS es representado por una estructura del kernel llamada inodo. Cada vezque un archivo necesite ser referenciado, el VFS creará una instancia de un inodo asociada al archivoen cuestión. Por ende, una buena decisión es ubicar la clase de seguridad de un archivo dentro de suinodo correspondiente en el VFS. Un problema inmediato que surge es que luego de reiniciar el sistemalos inodos creados por el VFS se pierden, y por lo tanto, también la clase de seguridad que conteníaéste. Esto nos lleva a tratar de encontrar alguna forma de lograr la persistencia de nuestro atributo deseguridad.

La forma más directa y sencilla es agregar también este atributo en la estructura que identi�caal archivo en el dispositivo, o sea, guardar la clase de acceso en disco. Como el prototipo de basainicialmente en el sistema de archivos ext3, esta no es una tarea muy difícil de realizar ya que ext3 usatambién al inodo como estructura principal para sus archivos. Precisamente utilizaremos los atributosextendidos introducidos en la sección 4.5.

2Toda proceso en el sistema, inicialmente, arranca con la menor clase posible.3Para más detalles sobre el VFS ver sección 4.14.2.

Page 87: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 85

5.1.1. Resumen

Los pasos necesarios para implementar los requerimientos recién comentados son:

1. Agregar al descriptor que representa un proceso (task_struct) un atributo que describa la clasede acceso del proceso.

2. Agregar también al descriptor una lista que contenga los s-hermanos del proceso representado.

3. Modi�car la llamada al sistema sys_read(). Cuando un proceso P haga un read() sobre un archivoA:

Si scA � scP o las sc no sean comparables:

• Comprobar si P no tiene un s-hermano S con scS = scA

◦ Si lo tiene retornar como si no se puede leer más del archivo, o sea 0. Este valorcomúnmente es devuelto cuando se lee un EOF (End Of File).

◦ Si no, crear un s-hermano, registrarlo para que todos los s-hermanos de P sepan dela existencia de este4. Luego, con�gurar el program counter (PC) del nuevo procesocreado en uno menos (una rutina menos) que su padre. Esto se debe realizar paraque cuando el proceso hijo inicie su ejecución llame nuevamente a sys_read().Finalmente, Asignar el nuevo proceso a otro core y retornar del proceso originalcomo se detalla en el item anterior.

Si scA � scP se debe realizar la llamada al sistema read() normalmente.

4. Modi�car la llamada al sistema sys_write() para que solo se pueda escribir en un archivo Asiguiendo la siguiente regla:

Dada la clase de acceso de A, scA, entonces puede escribir únicamente el s-hermano cuyaclase de acceso sea igual a scA. Si no existe un s-hermano con esa clase de acceso, entoncesescribe el s-hermano cuya clase de acceso es Sup{sc: clase de acceso | sc es la clase de accesode un s-hermano con sc � scA}.

Dada esta regla, si el proceso no puede escribir igualmente retorna como si hubiese podidohacerlo, devolviéndole la cantidad de bytes que requirió escribir en un principio.

En las secciones siguientes se abordarán más extensamente cada uno de estos items, enumerandolos problemas surgidos durante su implementación, sus soluciones o los caminos alternativos tomados.

5.2. Agregar el atributo de seguridad a los procesos

Debemos agregar un campo en la task_struct que identi�que la clase de acceso de ese proceso. Comoprimera instancia creamos un TAD llamado �owx_security_struct de�nido en �owx/include/fxobj.h.Como el kernel de Linux está programado en C por cuestiones de performance y nuestro módulotambién, la forma más e�ciente de crear un TAD en C es de�nir una estructura y una serie de rutinaspara manipular los campos de está. También se deben proveer funciones para crear y destruir instanciasde esta estructura.

typedef struct sc {

__u32 level;

__u32 categories[CAT_NUM];

} sc;

4La propiedad s-hermano de es transitiva.

Page 88: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 86

struct flowx_security_struct {

sc *sc;

struct list_head s_siblings;

struct task_struct *my_task;

};

Los campos de la �owx_security_struct son:

Un puntero a una estructura de tipo sc (security class). Este tipo, de�nido en �owx/inclu-de/sc.h, guardará la información especí�ca de la clase de acceso. Básicamente en los campos levely categories[] de la struct sc.

Una lista doblemente enlazada de tipo list_head (de�nido en include/linux/list.h). Este camposervirá para enlazar los diferentes procesos que sean s-hermanos5.

Un puntero a una task_struct de�nida en include/linux/sched.h. Referencia a la task_structportadora de la instancia de �owx_security_struct.

Anteriormente, mencionamos que LSM nos brindaba un hook para poder alojar un campo adi-cional en la task_struct. Este campo, void* security forma parte permanente de la estructura y sufunción es guardar la dirección de cualquier tipo de dato necesario para implementar un modelode seguridad dado. Cada vez que se crea un proceso nuevo a través de las llamadas sys_clone(),sys_fork() o sys_vfork(), se aloja este nuevo campo a través de la implementación de la hook se-curity_task_alloc_security. Todas las de�niciones, hooks y comportamientos por defecto de LSM seencuentran en include/linux/security.h. La secuencia de llamadas dentro del kernel para alojar el cam-po security se muestra a continuación:

sys_fork() ; arch/i386/kernel/process.c⇓

do_fork() ; kernel/fork.c⇓

copy_process() ; kernel/fork.c⇓

security_task_alloc_security() ; include/linux/security.h

Nuestra implementación para el hook task_alloc_security es la función �owx_task_alloc_security.Esta función inicializa una estructura del tipo �owx_security_struct y asigna al campo security sudirección. Todo proceso cuando recién nazca tendrá un nivel de seguridad igual al mínimo y ningunacategoría asignada. La idea es que los procesos vayan adquiriendo mayores clases de acceso a medidaque van siendo creados como s-hermanos de algún otro. Aparte del hook para alojar una estructura enel campo security existe otro para desalojarla llamado security_task_free_security. Este se invoca almomento de la destrucción de un proceso y normalmente su útilidad es liberar el espacio de memoriarequerido para el dato al que apunte el campo security. Esto es exactamente lo que hace nuestraimplementación �owx_task_free_security, al liberar la instancia de �owx_security_struct.

5.3. Agregar lista de s-hermanos

Como mencionamos en el apartado anterior, la estructura �owx_security_struct que se agrega alcampo security de la task_struct tiene un atributo para enlazar entre sí a los s-hermanos de un proceso.

5Para ver cómo funcionan las listas en el kernel ver 4.14.1.

Page 89: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 87

A través de este campo, s-sibling, y gracias a la API de�nida en include/linux/list.h, se pueden recorrery recuperar las task_struct correspondientes a los s-hermanos de un proceso dado.

5.4. Agregar el atributo de seguridad al inodo de VFS y ext3

Nuestro modelo de seguridad lidia todo el tiempo con relaciones sujeto-objeto, en donde las de-cisiones de cuáles interacciones de éstas permitir y cuáles no, constituyen el punto de apoyo paraasegurar la con�dencialidad de los datos. Como ya mencionamos anteriormente para este primer pro-totipo, nuestra noción de objeto es el archivo. Cada archivo está identi�cado en el kernel por una únicaestructura llamada inodo la cuál etiquetaremos con una clase de seguridad dada. En está sección secomentarán los detalles de cómo se etiquetó cada inodo (tanto del VFS como del sistema de archivosext3 ) y quiénes están autorizados a cambiar estos atributos de seguridad y cómo lo hacen.

Tenemos dos tipos de inodos que tratar, el perteneciente al VFS y el que pertenece al ext3. Elprimero será un inodo temporal que crea Linux cada vez que se referencia a un inodo real y concretoexistente en algún dispositivo dado, como puede ser un cd-rom, un disco rígido, etc. El segundo seráuno de estos inodos reales que, por su naturaleza, no será eliminado en cada reinicio del sistema, yaque siempre reside en su dispositivo asociado. El inodo VFS reside en memoria y duplica parte de lainformación residente en disco perteneciente a su par, el inodo ext3. Todas las operaciones sobre unarchivo (salvo casos especiales) se llevan a cabo sobre los datos temporales del inodo VFS y cuando seanecesario, esa copia será actualizada con la imagen en disco del inodo en cuestión. De esta forma, elacceso a un archivo se realiza de forma más rápida y e�ciente, ya que no se requieren múltiples accesosal disco por cada modi�cación a un archivo.

Principalmente tenemos dos opciones, la primera consiste en agregar uno o varios campos en laestructura inodo del sistema de archivos. Por ende, es lógico pensar que podemos implementar lapersistencia de nuestro atributo alocando un campo en la estructura ext3_inode (de�nida en /in-clude/linux/ext3_fs.h). Sin embargo, agregar nuevos atributos a la estructura del inodo, signi�ca elaumento en el tamaño de éste y, obviamente, un cambio en el inodo en sí. Problemas de compatibilidadsurgirían inmediatamente en programas que usan esta estructura como base para la mayoría de losservicios que proveen. Un ejemplo concreto, es el analizador y reparador de errores en sistemas dearchivos de Linux, fsck. Para que el prototipo mantuviese la compatibilidad había que modi�car estosprogramas para que puedan correr sobre este nuevo sistema de archivo, lo que no nos pareció adecuado.Por lo tanto usamos los atributos extendidos de�nidos para ext2, ext3, xfs y raiserfs, a partir del kernel2.6, introducidos en la sección 4.5.

A continuación presentamos en detalle cómo se agrega la clase de acceso a nivel ext3 (sección 5.4.1)y a nivel de VFS (sección 5.4.2).

5.4.1. Usando los xattr

Nuestro uso de los xattr se centra sobre todo en la implementación del hook security_inode_init_-security(), el cual sirve para mediar en la creación de un inodo en el sistema de archivos6. Este hook esllamado por la función ext3_init_security() en fs/ext3/xattr_security.c, la cual se encarga de iniciarun atributo extendido relativo a la seguridad en el inodo a crear. Dentro del hook, el programador debeproporcionar el nombre y valor del nuevo atributo. Si no se devuelve -EOPNOTSUPP7 se creará unatributo extendido cuyo nombre consistirá en el pre�jo �security.�8 concatenado con el nuestro, el cualsera ��owx� y lo de�ne la macro FLOWX_XATTR_SUFFIX.

6Notar que no media en la creación de un inodo de VFS, de hecho, en este caso, el inodo de VFS se crea primero ycon parte de la información de éste se crea el de ext3.

7Ver include/asm-generic/errno.h8De�nido por la macro XATTR_SECURITY_PREFIX en include/linux/xattr.h.

Page 90: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 88

Para almacenar la clase de acceso en los archivos usamos solamente la estructura sc, por lo cual,security_inode_init_security() o especí�camente nuestra implementación de este hook, �owx_inode_-init_security(), alojará una sc como valor del atributo extendido de seguridad. Además, como el modeloexige que todo archivo recientemente creado tenga la clase de seguridad mínima, el valor siempre seráel mismo.

El xattr de seguridad puede ser modi�cado desde espacio de usuario, pero para ello se debensatisfacer las siguientes condiciones:

El proceso que requiere un cambio en el atributo con nombre �security.�owx� debe ser un admin-istrador de seguridad o el archivo debe estar vacío. Qué es y cómo se distingue un administradorde seguridad se explicará el capítulo 9; en cuanto a si el archivo está vacío o no, se puede conocera través del campo i_size del inodo VFS asociado a éste.

El proceso debe ser el resultado de ejecutar un programa en la TCB ; este punto se detalla en elcapítulo 9.

El chequeo de estas condiciones lo lleva a cabo la función �owx_inode_setxattr() (implementación delhook security_inode_setxattr()), la cual retorna 0 si permite la operación y -EPERM en caso contrario.

El prototipo necesita consultar en varias funciones el valor del atributo extendido de seguridad. Lasección 5.4.2 explica un detalle de la implementación en donde está lectura ejerce un papel crucial. Lafunción exportada por el kernel vfs_getxattr():

ssize_t vfs_getxattr (struct dentry *dentry, char *name, void *value, size_t size);

es la función principal usada por sys_getxattr() para obtener un xattr y la que nosotros usaremos paraesta tarea.

Por último, LSM provee varios hooks para el caso en que se quiera operar con atributos extendidosen dispositivos que no los soportan. security_inode_setsecurity() y security_inode_getsecurity() sonun par de estos hooks, útiles para tomar otro curso de acción en el caso de que el dispositivo noimplemente estos atributos. Chequear que inode->i_op->getxattr no sea nulo es una buena forma deaveriguar si los xattr son soportados. Como el prototipo de Flowx será implementado inicialmente enext3, no debemos preocuparnos por esto, pero es útil notarlo para etapas posteriores.

5.4.2. El campo i_security del inodo de VFS

Tener nuestra clase de seguridad almacenada en los archivos de nuestro sistema a través de losatributos extendidos, soluciona el problema de la persistencia. Sin embargo, debemos encontrar algunaforma para poder re�ejar este atributo en los inodos del VFS. Aquí es donde entra en juego el campoi_security de la estructura inodo. Este campo es agregado por LSM para poder etiquetar un inodo para�nes de seguridad y su tipo, void *, permite que éste apunte a cualquier tipo de variable que queramosde�nir. Particularmente, nosotros almacenaremos en i_security una estructura de�nida como sigue:

struct flowx_inode_struct {

sc sec;

short type;

void *security_data;

};

El campo sec contendrá el mismo tipo de etiqueta que se uso para el inode ext3 : la estructura sc.El secreto detrás de este campo es que siempre debe ser exactamente igual al del valor del xattr deseguridad que comentamos en la sección anterior. Por lo tanto, tendremos que tener especial cuidadoen mantener, siempre, este �enlace simbólico� entre el campo i_security y el atributo security.�owx.Debemos manejar dos posibles escenarios:

Page 91: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 89

La creación de un nuevo inodo VFS al referenciarse su par en ext3.

El cambio del atributo extendido de seguridad en un inodo ext3.

Para solucionar el primer item se usa un mecanismo muy sencillo. Cada vez que se aloja un nuevoinodo VFS se llama al hook security_inode_alloc(), el cual es invocado por la función inode_alloc()(en fs/inode.c) cuando el cache de inodos necesita alojar uno nuevo9. Este hook es el único respon-sable de inicializar el campo i_security en la creación de un inodo. En nuestra implementación,�owx_inode_alloc(), creamos una nueva estructura �owx_inode_struct, asociamos a su campo secel nivel de seguridad −1 y hacemos que i_security apunte a ésta. En nuestro módulo, toda solicituddel campo i_security se debe llevar a cabo a través de la función get_�le_sc(). Cuando esta rutinaobtiene la clase de acceso del inodo requerido debe chequear el valor del nivel de seguridad de dichaclase. Pueden ocurrir dos situaciones:

1. El nivel de seguridad es mayor o igual a 0 lo que indica que la clase de acceso de inodo ya hasido inicializada y se retorna la clase obtenida.

2. El nivel menor a 0, lo que signi�ca que la clase no ha sido inicializada aún. Para inicializar unaclase simplemente debemos veri�car si no posee un atributo extendido de seguridad inicializadopreviamente. La función vfs_getxattr() realizará esta tarea y su valor de retorno nos informaráde la existencia o no del atributo buscado. En conclusión, si el xattr existe, la nueva sc será unacopia de su valor, en caso contrario, la inicializaremos con la menor clase de seguridad posible.

Cambiar el xattr de seguridad en un inodo ext3 signi�ca que tendremos que re�ejar este cambioen el campo i_security del inodo VFS asociado. El hook security_inode_post_setxattr() es ideal paracubrir este aspecto ya que es siempre invocado por la función vfs_setxattr() inmediatamente después decrear y/o alterar cualquier atributo extendido. �owx_ inode_post_setxattr() intercede únicamente si elnombre del atributo en cuestión es el mismo de�nido por la macro FLOWX_SECURITY_XATTR_-NAME y simplemente actualiza i_security con el nuevo valor.

Los otros dos campos de la estructura �owx_inode_struct sirven para identi�car y administrar eltipo que recurso que representa el inodo afín. Al principio de la sección dijimos que un inodo VFSrepresenta una porción de memoria asociada a un archivo real guardado en disco. Sin embargo estoúltimo no es del todo cierto, ya que puede haber inodos no asociados a archivos en disco. Existen otrostipos de recursos del kernel que utilizan la memoria asociada al inodo y de�niendo nuevas rutinas parael acceso a ésta, implementan su funcionalidad. Los recursos que utilizaremos serán:

Archivos especiales de dispositivos Capítulo 8.

Pipes y FIFOS Capítulo 10, Sección 10.1.

Colas de mensajes de POSIX Capítulo 10, Sección 10.4.

Memoria compartida Capítulo 10, Sección 4.12.3.3.

Mapeos de archivos de disco en memoria Capítulo 10, Sección 10.3.

Para imponer la seguridad de cada uno de estos recursos necesitaremos estructuras especiales quenos con�eran información especí�ca del recurso en cuestión. El campo security_data servirá paraalmacenar esta información y el campo type nos ayudará a descubrir qué tipo de objeto almacena elpuntero en un momento dado. Por ejemplo en el hook security_inode_free(), cuando debamos liberarla estructura �owx_inode_struct, tendremos que identi�car el tipo de dato alojado en securiy_datapara invocar la rutina necesaria para también liberar esta estructura.

9Para más información sobre el inode cache ver [16].

Page 92: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 90

Como último detalle sobre i_security, notar que una vez que un inodo VFS ha sido creado, éste noes borrado de las estructuras internas del kernel hasta que el sistema de archivos no sea desmontadoo se borre su archivo asociado. El inodo pasará de lista en lista en un cache interno dependiendosu estado, pero siempre permanecerá. Tener la clase de acceso duplicada en los dos inodos tiene dosventajas: persistencia, al saber que la clase siempre quedará guardada en disco a través del atributoextendido; y performance, al tener la copia del atributo persistente guardada en el campo i_securityy tener que acceder solo una vez al disco para obtenerla. Sin i_security tendríamos que realizar unalectura por cada comparación de clases entre procesos y archivos.

5.5. Modi�car sys_read()

La modi�cación de esta llamada, como se dijo anteriormente, es la base para el correcto fun-cionamiento del módulo. Para interferir esta llamada (de�nida en fs/read_write.c) se usó el hook se-curity_�le_permission() el cual recibe como argumento el archivo que se quiere leer y si la operaciónes de escritura o lectura. Este hook está tanto en sys_read() como en sys_write(), lo que justi�cala existencia de este segundo parámetro. Nuestra implementación de security_�le_permission() es�owx_�le_permission(), de cuyo desarrollo se trata la mayoría de este apartado.

El prototipo de security_�le_permission() es la raíz de nuestro primer problema. Como nuestraimplementación debe ajustarse a los prototipos de las diferentes funciones hooks, surgen problemasal requerir información que a veces es inalcanzable. Un ejemplo concreto es que no tenemos ningúnmecanismo para obtener la dirección del bu�er en donde la llamada read() debe devolver la cadenaleída. Tampoco tenemos información de cuántos bytes se pidieron de lectura. Esto torna imposible poderdevolver la cadena de basura dentro del bu�er pasado por el usuario ni retornar el valor de los bytesrequerido inicialmente para simbolizar un éxito en la llamada. Debido a esto, devolveremos como si elarchivo no tuviera más información que leer (EOF)10. Como este es solo un primer prototipo decidimosque el bu�er retorne tal y como era antes de la llamada y retornar un valor negativo que no coincida conlos reservados para la variable errno (ver include/linux/errno.h e include/asm-generic/errno-base.h).Lamentablemente, en este paso tendremos que tocar un poco el kernel para poder implementar elrequerimiento. Lo único que debemos hacer es chequear el valor que retorna el hook �le_permission()y si dicho valor es negativo y corresponde al valor que reservamos de errno para indicar la falta depermisos, devolver 0 (EOF) como resultado de la llamada sys_read().

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)

{

ssize_t ret;

if (!(file->f_mode & FMODE_READ))

return -EBADF;

if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))

return -EINVAL;

if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))

return -EFAULT;

ret = rw_verify_area(READ, file, pos, count);

if (ret >= 0) {

count = ret;

ret = security_file_permission (file, MAY_READ);

//=========================== FLOWX ==============================

if (ret == -35)

10Recordar las exigencias para sys_read() en caso de no tener acceso al archivo.

Page 93: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 91

return 0;

//=========================== FLOWX ==============================

if (!ret) {

if (file->f_op->read)

ret = file->f_op->read(file, buf, count, pos);

else

ret = do_sync_read(file, buf, count, pos);

if (ret > 0) {

fsnotify_access(file->f_path.dentry);

add_rchar(current, ret);

}

inc_syscr(current);

}

}

return ret;

}

La primera tarea que �owx_�le_permission debe realizar es la comparación entre la clase deacceso del proceso actual, cuya dirección de su task_struct siempre está almacenada en la variableglobal current11. La clase de acceso del archivo a leer, es la misma que la clase de acceso de suinodo asociado. Todo archivo abierto es representado en el kernel por la estructura �le, de�nida eninclude/linux/fs.h. Dado un �le obtener la clase de su inodo asociado es obtener la clase del inodoasociado al dentry12 de �le:

file->f_dentry->d_inode->i_security

La comparación se hace a través de las rutinas que se de�nieron en el TAD sc y se toman diferentescursos de acción a partir del resultado dependiendo de si se quiere leer o escribir en el archivo.

La parte trivial de la función es cuando se cumple la condición para el acceso al archivo o cuandono, pero existe un s-hermano con igual clase que la de �le. Si ocurre la primera simplemente se retornaun 0 y, con esto, se da vía libre para que sys_read() prosiga normalmente con su ejecución. Si ocurrela segunda opción, se retorna el valor negativo convenido y sys_read() retornará a modo de usuariosin haber hecho nada. La idea detrás de esto es que se sabe que los s-hermanos son, al momento de sucreación, una copia exacta de su padre. Eventualmente, tanto padre como hijo, ejecutarán las mismaslíneas de código pero con la diferencia de que cada uno tendrá una clase de seguridad diferente. Elhelper �nd_s_sibling() asiste a �owx_�le_permission() en la búsqueda de este s-hermano. Si currentno tiene acceso ni un s-hermano que tenga acceso implica lo siguiente:

1. Crear un nuevo proceso, exactamente igual a current.

2. Cambiar el Program Counter de este para que vuelva a ejecutar sys_read().

3. Asignar el nuevo proceso para que se ejecute en otro procesador (si lo hubiese).

A continuación detallaremos la implementación de cada paso.

5.5.1. Duplicar current

Para duplicar current nos basamos mucho en cómo el kernel duplica procesos a través de lasllamadas al sistema sys_fork() o sys_clone():

11Para más información de cómo current siempre apunta al proceso actual leer 4.14.5.12Para más información de qué es un dentry ver 4.14.3.

Page 94: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 92

asmlinkage int sys_fork(struct pt_regs regs);

Para duplicar el proceso, sys_fork() llama al helper do_fork(), el cuál se encarga de solicitarun nuevo pid13 para el hijo con alloc_pid() (de�nida en kernel/pid.c) y luego llama a la funcióncopy_process() para hacer todo el trabajo pesado de duplicar la task_struct y los registros del CPUque el kernel guardó para current al entrar en modo kernel. Estos registros están almacenados enla estructura pt_regs pasada inicialmente a sys_fork(). Para llamar a esta llamada desde el espaciode usuario se usa normalmente la función fork() de la libc. Como se puede apreciar, está función norequiere ningún tipo de parámetro para ser invocada, entonces: ¾cómo sys_fork() obtiene el parámetropt_regs si no se le especi�có ninguno? Y de ésta surge otra más directa: ¾qué es la estructura pt_regs?Antes de contestar repasemos un poco qué hace el kernel cuando se invoca a una llamada al sistema.Para invocar una syscall, desde espacio de usuario, se pueden usar dos métodos:

La interrupción 0x80.

La instrucción de assembler sysenter.

Basaremos nuestra explicación en el primer método, ya que es más simple de entender, y a pesar desus diferencias, para lo que queremos mostrar, hacen básicamente lo mismo.

Cuando la interrupción es llamada a través de la instrucción int $0*80, el manejador de interrup-ciones del kernel chequea en la Tabla de Descriptores de Interrupciones para ver qué handler llamar paradicha interrupción. Particularmente, para la int 0x80 el handler es la función system_call() de�nidaen arch/i386/kernel/entry.S. system_call realiza más o menos lo siguiente:

1. Pone el valor que tenga eax en la Kernel Mode Stack14. Normalmente eax contiene el númerode la syscall a ser llamada. Para ver qué número corresponde a qué syscall ver include/asm-i386/unistd.h.

2. Llama a la macro SAVE_ALL, la cual pone en la pila el valor de cada uno de los registros deCPU, salvo por e�ags, cs, eip, ss y esp que ya fueron puestos anteriormente por el CPU comoresultado de llamar a un interrupción. El orden es muy importante, ya que re�eja la ubicación delos campos de la estructura pt_regs de�nida en include/asm-i386/ptrace.h y que explicaremosmás adelante.

3. Luego de hacer varios chequeos para ver si el número de llamada al sistema no es incorrecto o sise debe llevar a cabo un seguimiento del proceso a través de la llamada (ej. en presencia de undebugger), se llama al handler de la syscall, cuyo símbolo esta almacenado en la sys_call_tableen arch/i386/kernel/syscall_table.S.

4. Luego de otros chequeos pertinentes a bottom halves15 pendientes, entre otras cosas, se llama a lamacro RESTORE_ALL que realiza el camino inverso que SAVE_ALL, restaurando los registrosprincipales para que el proceso pueda resumir su ejecución en espacio de usuario normalmente.

5. Llama a iret para retornar a espacio de usuario.

Con el trasfondo dado intentemos responder nuestras preguntas iniciales. Todos los manejadoresde las llamadas al sistema en el kernel son de�nidos con la macro de gcc asmlinkage de�nida eninclude/linux/linkage.h. Esta macro le dice a gcc que busqué los argumentos de la función en la pilay no en los registros, donde fueron pasados originalmente (ebx, ecx, edx, edi, esi en ese orden). Elregistro esp, antes de que la función system_call llame al handler especi�co, apunta a la ubicación del

13Process ID.14La Kernel Mode Stack es una pila que aloja en memoria el kernel para todo proceso que esté ejecutándose dentro

de él. Es única para cada proceso.15Para más información sobre bottom halves ver 4.14.4

Page 95: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 93

valor del último registro guardado por la macro SAVE_ALL. Al llamarse al handler, gcc llenará losargumentos de éste con los valores que encuentren desde esp hasta esp más el tamaño total de todoslos argumentos.

La estructura pt_regs está de�nida como sigue:

struct pt_regs{

long ebx;

long ecx;

long edx;

long esi;

long edi;

long ebp;

long eax;

int xds;

int xes;

int xfs;

long orig_eax;

long eip;

int xcs;

long eflags;

long esp;

int xss;

};

Por el orden en que SAVE_ALL guarda los registros y como en esta arquitectura todos los registrostienen el mismo tamaño (long o int), gcc cargará la pt_regs de manera que lo guardado en la pilapara el registro ebx sea almacenado en el campo ebx, lo guardado para ecx en el campo ecx, y asísucesivamente. Por lo tanto, en respuesta a la pregunta de qué es pt_regs, podemos decir que es unaestructura que guarda el estado de los registros de un proceso dado antes de entrar en modo kernel.Aunque uno puede modi�car o consultar estos valores manipulando directamente la Kernel Mode Stackdel proceso usando rutinas en assembler, manipular estos valores a través de pt_regs es mucho másintuitivo, fácil y menos propenso a errores. Una función útil para obtener pt_regs de un proceso estask_pt_regs() de�nida en include/asm-i386/processor.h.

Ahora que tenemos el conocimiento necesario sobre pt_regs podemos proseguir en la duplicaciónde current. Como dijimos anteriormente la rutina do_fork() es la encargada de duplicar el proceso,con la asistencia de la función copy_process(). La primera de las modi�caciones necesarias al núcleose realiza en este paso al tener que agregar código para poder exportar do_fork() para que pueda serllamada desde nuestro módulo. La implementación de �owx_�le_permission() llamará a la siguientefunción:

new_task = do_fork(0, regs, regs.esp, 0, 0);

donde:

El primer argumento dice que no queremos utilizar ninguna de las CLONE_FLAGS para laduplicación. Para más información sobre estas banderas ver [14].

El segundo es la estructura pt_regs perteneciente al proceso a ser duplicado. Para obtener unacopia de esta, usamos la función task_pt_regs().

El tercer argumento indica a dónde queremos que apunte el registro esp cuando el nuevo procesocreado resuma su ejecución en espacio de usuario. Al pasarle el mismo valor que el de su padre,nos aseguramos que el hijo empiece llamando a la siguiente instrucción que el padre tendría queejecutar al volver del modo kernel.

Page 96: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 94

El cuarto no se usa más, antes simbolizaba el tamaño de la pila. Es siempre 0.

El restante no tiene demasiado importancia para nuestro uso y, por ende, se asigna nulo y no seexplicará en este documento.

new_task() es una variable de tipo task_struct y se usa para almacenar el identi�cador del nuevoproceso creado.

5.5.2. Modi�car el Program Counter

Una vez duplicado el proceso, el paso siguiente consta en encontrar una forma para que el nuevoproceso ejecute nuevamente la llamada sys_read(). Para poder hacer esto tendremos que modi�car elProgram Counter que es un registro que contiene la dirección de la próxima instrucción a ser ejecutada.Luego de cada ciclo de instrucción el PC necesita ser actualizado para apuntar a la siguiente instrucciónen memoria. Para los procesadores RISC esto es fácil ya que todas las instrucciones miden 32 bits.El nuevo PC será simplemente igual al anterior más 4 bytes16. En cambio, en los CISC, el codi�cadorde instrucciones debe calcular el largo de la instrucción actual y sumar ese valor al PC (en bytes).En la arquitectura x86 el PC se encuentra en el registro eip (instruction pointer). Veamos cómo laarquitectura x86 maneja el eip.

Las excepciones son eventos causados por errores de programación o por condiciones anómalasdetectadas por el CPU y que deben ser manejados por el kernel. Al igual que las interrupciones, cadaexcepción posee un manejador asociado que debe ser invocado cada vez que alguno de estos eventosocurre. Las excepciones detectas por el CPU se pueden dividir en tres grandes grupos dependiendo delvalor del registro eip que es guardado en la Kernel Mode Stack cuando la unidad de control del CPUlevanta la excepción.

Faults

Pueden, generalmente, ser corregidas; una vez corregidas, el programa puede resumirse sin pérdidade continuidad. El valor del eip guardado corresponde al de la instrucción que generó la falla.

Traps

Reportadas inmediatamente después de la ejecución de una instrucción que genere una; luego deque el kernel retorne el control se permite al programa que resuma su ejecución. El valor guardadoen eip es la dirección de la instrucción que debería ejecutarse luego de aquella que produjo latrampa.

Aborts

Un problema serio ha ocurrido; la unidad de control está en problemas y puede que ésta no puedaalmacenar en el eip la posición exacta de la instrucción que produjo la excepción.

Hay excepciones programadas que ocurren cuando son solicitadas por el programador. Son disparadaspor la instrucción int o int3 y la unidad de control del CPU las maneja como si fueran trampas. Tienendos usos comunes: implementar llamadas al sistema o noti�car al debugger un evento especí�co.

Como puede verse, nuestro caso (excepción programada) el valor que tendrá el eip guardado porel CPU y accesible a través de la estructura pt_regs, será el de la próxima instrucción a ejecutarse.Para una syscall invocada usando int $0x80, el eip será la dirección de cmpl $-125,%eax en el ejemplomostrado abajo.

16Tanto en CISC como en RISC la memoria es byte-addressable, o sea, está dividida en bytes.

Page 97: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 95

read:

pushl %ebx ; Pone ebx en la pila

movl 8(%esp), %ebx ; Pone el primer parámetro en ebx.

movl 12(%esp), %ecx ; Pone el segundo parámetro en ecx.

movl 16(%esp), %edx ; Pone el tercer parámetro en edx.

movl $3, %eax ; Pone __NR_read en eax.

int $080 ; Invoca a la syscall.

cmpl $-125, %eax ; Chequea el valor de retorno.

jbe .L1 ; Si no hay error, salta.

negl %eax ; Complementar el valor de eax.

movl %eax, errno ; Poner el resultado en errno.

movl $-1, %eax ; Setea eax en -1.

.L1:

popl %ebx ; Saca ebx de la pila.

ret ; Retorna al programa invocador.

Teniendo en cuenta esto esto, hacer que nuestro proceso vuelva a ejecutar la llamada read implicarestar al eip almacenado en la Kernel Mode Stack, el tamaño que tenga la instrucción int $0x80 ennuestra arquitectura. Pero esto no es tan simple como parece:

¾Cómo saber cuánto mide la instrucción sin tener que dejar �jo en el código un valor obtenidoexternamente?

¾Qué pasa si la syscall se invocó a través de la instrucción sysenter?

No solo habría que modi�car el eip, sino también el registro eax y el registro e�ags que sonmodi�cados cuando se duplica un proceso y cuya explicación daremos en breve.

Al analizar estos inconvenientes y descubrir que su solución implicaba cierta di�cultad, decidimosutilizar otro enfoque para lograr nuestro objetivo inicial.

Es obvio que nuestro modelo requiere sí o sí que el nuevo proceso llame nuevamente a sys_read()pero podemos hacerlo sin, necesariamente, tener que modi�car el program counter del proceso enespacio de usuario. Lo que pretendemos lograr es que nuestro s-hermano recientemente creado, puedarealizar la lectura del archivo sin tener que volver a espacio de usuario para hacerlo. La idea es quesys_read() sea invocada nuevamente sin abandonar modo kernel asignándole al thread pertenecienteal nuevo proceso un nuevo camino para retornar a modo usuario.

Cuando discutíamos sobre cómo se duplica un proceso, mencionamos la función copy_process()cuya objetivo era duplicar la task_struct. copy_process() se apoya en otra función de�nida en arch/-i386/kernel/process.c llamada copy_thread(). Esta función inicializa la Kernel Mode Stack del nuevoproceso con los valores de los registros del CPU de la pt_regs del padre. El campo thread, en eldescriptor del proceso, es un campo destinado a guardar la información sobre un thread dentro delkernel cuando ocurre un cambio de contexto. Al Cambiar ciertos campos en esta estructura estaremosafectando al thread perteneciente a nuestro nuevo proceso cuando se decida su ejecución. copy_thread()fuerza a que el campo correspondiente al registro eax, thread.eax, sea 017. También, el campo thread.espes inicializado con la dirección de la base de la Kernel Mode Stack del hijo y la dirección de una funciónen assembler (ret_from_fork() en entry.S ) es almacenada en thread.eip. Esto último hace que cuandoel scheduler decida correr al nuevo proceso (todavía en modo kernel), este empezará ejecutando la

17Cuando se llama a fork() en espacio de usuario, al padre se le retorna el pid del nuevo proceso hijo, mientra que alhijo se le retorna un 0.

Page 98: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 96

instrucción almacenada en thread.eip. Entonces, parte de la solución yace en poner el eip al valor de ladirección de ret_from_fork()18. Sin embargo, esto no es su�ciente pues meramente asignar un nuevovalor a eip no lo pondrá al proceso por el camino deseado. El truco reside en cómo Linux producelos cambios de contextos usando la rutina switch_to() de�nida en include/asm-i386/system.h19. Deeste modo, dos threads irán por caminos de retorno diferentes en una llamada fork(); el padre irá porret_from_sys_call y el hijo por ret_from_fork, la cuál se de�ne como sigue:

ENTRY(ret_from_fork)

pushl %eax

call schedule_tail

GET_THREAD_INFO(%ebp)

popl %eax

pushl $0x0202 # Reset kernel eflags

popfl

jmp syscall_exit

END(ret_from_fork)

Básicamente lo que hace es:

Poner en la pila el valor del registro eax. Siempre después de un cambio de contexto este registroapunta a la dirección del proceso que cedió el CPU a current. Como sabemos que lo primero queva a ejecutar un nuevo proceso creado con fork() es esta rutina, nos aseguramos de que el registroestá intacto.

Llamar a schedule_tail() (kernel/sched.c), función que debe ser llamada siempre luego de uncambio de contexto. Maneja los últimos detalles del cambio de contexto reseteando el campohas_cpu en el proceso argumento y también haciendo que se reprograme. Como la función estáde�nida con la macro asmlinkage, ésta tomará los argumentos de la pila. En este caso, su únicoargumento de tipo task_struct será el que ya está en la pila y coincide con el proceso saliente.

Cargar thread_info en el ebp.

Resetear e�ags (registro especial en los procesadores x86 que contiene el estado actual del proce-sador).

Llamar a syscall_exit para retornar a espacio de usuario.

Volviendo a nuestra implementación, invocar a sys_read() nuevamente desde modo kernel se basaen de�nir una función de similares características que ret_from_fork. Para poder agregar esta rutinadebemos modi�car nuevamente el kernel, esta vez tanto en entry.S donde se de�nirá la rutina deassembler ret_from_�owx :

ENTRY(ret_from_flowx)

pushl %eax

call schedule_tail

GET_THREAD_INFO(%ebp)

popl %eax

call sys_read

movl %eax,PT_EAX(%esp)

jmp syscall_exit

END(ret_from_flowx)

18Es el camino que tomará el nuevo proceso en en espacio kernel. Como sabemos, en espacio de usuario tomará elmismo que el padre.

19Para más información sobre cambios de contexto leer 4.14.6.

Page 99: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 97

y en arch/i386/kernel/process.c con las funciones C ret_from_�owx(), útil para poder invocar a sutocaya en assembler, y �owx_path, que será la única exportada al módulo con el �n de modi�car eleip del nuevo thread:

asmlinkage void ret_from_flowx(void) __asm__("ret_from_flowx");

void flowx_path (struct task_struct *p)

{

p->thread.eip = (unsigned long) ret_from_flowx;

}

EXPORT_SYMBOL(flowx_path);

En ret_from_�owx se empieza llamando obligatoriamente a schedule_tail() y obteniendo la thread_infoen ebp como en ret_from_fork. La diferencia con ésta radica en no recon�gurar e�ags, para que el proce-sador esté en el mismo estado que tenía el proceso padre cuando estaba en modo kernel, y en llamar,antes de salir a través de syscall_exit, a sys_read() otra vez. Notar que movl%eax,PT_EAX(%esp)sirve para guardar el valor de retorno de sys_read(), almacenado por convención en eax, en el campode pt_regs correspondiente a éste.

Page 100: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 98

Figura 5.1: Caminos en la de creación de un s-hermano

En nuestra función �owx_�le_permission() también cambiamos el valor del registro eax para queno sea 0 como asignó copy_thread, sino el primer valor de eax almacenado en la pila cuando se entróen el kernel y que contiene el número de syscall a ejecutar. Este valor se obtiene muy fácilmente yaque está referenciado por el campo orig_eax en la pt_regs. Esta modi�cación permite que el hijotenga exactamente los mismos valores en su pt_regs que en la de su padre. Recordemos que, comosys_ read esta de�nida con la macro asmlinkage, tomará sus argumentos de la pila, y si nos �jamosen cómo la macro SAVE_ALL guarda los registros, los tres argumentos principales de está nuevallamada a sys_read() serán los que estén en 8((%esp), 12(%esp) y 16(%esp), o sea, ebx, ecx y edxrespectivamente, los cuales son los argumentos originales inicialmente pasados en la sys_read() original.

5.5.2.1. Pensando en el futuro

Es muy probable que en etapas posteriores también haya que crear s-hermanos al llamar a otrasllamadas al sistema diferentes que sys_read(). No discutiremos ahora cuáles son éstas llamadas, pero símostraremos un pequeño cambio a ret_from_�owx para que se pueda adaptar a cualquier llamada; osea, que el s-hermano que decida tomar el camino de ret_from_�owx haga lo que en esencia debe hacer,lo cual es ejecutar nuevamente la llamada que produjo su creación. Lograr esto es simple. Si observamosel handler para syscalls, system_call (en entry.S ), luego de guardar el estado de los registros y realizaralgunas comprobaciones sobre si el proceso debe ser seguido o no durante la llamada, se invoca lafunción encargada de llevar a cabo la syscall de la siguiente manera:

syscall_call:

call *sys_call_table(,%eax,4)

movl %eax,PT_EAX(%esp) # store the return value

syscall_exit:

Page 101: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 99

DISABLE_INTERRUPTS(CLBR_ANY)

TRACE_IRQS_OFF

movl TI_flags(%ebp), %ecx

testw $_TIF_ALLWORK_MASK, %cx

jne syscall_exit_work

Lo único que la etiqueta syscall_call realiza es invocar a la función almacenada en la ubicacióneax de la sys_call_table. Una vez �nalizada la ejecución de esta función se actualiza el valor de eaxen pt_regs con el valor de retorno y por último se prosigue a través de la etiqueta syscall_exit paradesarrollar todos los tramites necesarios para volver a espacio de usuario. Si uno compara la parte �nalde ret_from_�owx con syscall_call se dará cuenta que la única diferencia es que, en vez de llamar ala función en la sys_call _table se llama directamente a sys_read. Para generalizar nuestro camino deretorno debemos hacer exactamente lo que syscall_call hace, saltando a esta etiqueta luego de sched-ule_tail. Sin embargo recordemos que eax no contiene el número de syscall que queremos invocar, sinola dirección del último proceso que tuvo posesión del CPU. La sentencia movl PT_EAX(%esp),%eaxhace este trabajo por nosotros, como se puede observar en el código que sigue:

ENTRY(ret_from_flowx)

pushl %eax

call schedule_tail

GET_THREAD_INFO(%ebp)

popl %eax

movl PT_EAX(%esp),%eax

jmp syscall_call

END(ret_from_flowx)

5.5.3. Asignar el nuevo proceso a otro core

El último paso que se debe tomar una vez duplicado el proceso es asignarlo a la runqueue de otrocore. Hay varias formas de hacer esto:

Usando la función enqueue_task() (de�nida en kernel/sched.c), de la siguiente forma: enqueue_-task(new-s-sibling, cpu_rq(n)), donde cpu_rq(n) devuelve la runqueue correspondiente al coren. Para obtener n se puede usar la rutina next_cpu(current->thread_info->cpu, current->-cpus_allowed), la cual devuelve el entero asociado al próximo CPU de los que estén disponibles.De esta forma nos libramos del caso en que haya múltiples cores. El problema con esto es que lafunción enqueue_task() no está exportada para que la usen módulos externos.

La función wake_up_process() (exportada por el kernel) recibe una task_struct y despierta alproceso asociado a través de la rutina try_to_wake_up(). Esta función despierta un proceso yelije un CPU para ejecutarlo con la asistencia de wake_idle(), la cual retorna un CPU pasivoo en su defecto el CPU pasado como argumento, que en este caso será el CPU registrado enel s-hermano obtenido con task_cpu(). Por último try_to_wake_up() llama a activate_task()que se encarga de recalcular prioridades y agregar el proceso a la runqueue del CPU elegidoanteriormente.

No hacer nada y dejar que el proceso se despierte gracias al wake_up_new_task() que invocado_fork() como parte �nal de la duplicación. La función mencionada pondrá al nuevo task enla misma runqueue de su padre y por ende, no se lo asigna al otro core. Sin embargo, esto nosigni�ca que el proceso termine ejecutándose en el mismo CPU que el proceso original, ya queel kernel puede llegar a asignarlo a otro CPU debido a un balanceo de carga. De esta forma,relegamos en el kernel la responsabilidad de decidir en qué core se correrá el s_hermano.

Page 102: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 100

Nosotros decidimos optar por el tercer item, debido a que usamos do_fork() para duplicar unproceso. Intentamos también implementar las otras formas. Pero surgieron los siguientes problemas:

Teníamos que usar copy_process() para la duplicación y hacer aparte lo que do_fork() hacía pornosotros, o sea, obtener un pid nuevo y despertar al proceso nuevo, usando alguna técnica de lasmencionadas. Aunque la implementación no presentó ninguna di�cultad, a la hora de testear sesucedieron problemas en materia de recurrencia que exigían mucho esfuerzo para solucionar, porlo cual decidimos probar otro método.

Usar do_fork() pero con CLONE_FLAG CLONE_STOPPED activada. Esto hace que el procesonuevo empiece dormido y que nosotros tengamos que despertarlo manualmente o a través dealguna señal. Sin embargo, en la práctica, el proceso creado nunca se despertaba, a pesar deprobar diferentes formas de hacerlo y parecía quedar en un sueño eterno dentro del kernel.

5.6. Mo�car sys_write()

La modi�cación de la llamada sys_write() también se lleva a cabo a través del mismo hook secu-rity_�le_permission() o nuestro equivalente �owx_�le_permission(). Lo único que hay que hacer esagregar algunas sentencias iniciales a este hook para diferenciar si se trata de un read o un write graciasa la variable mode recibida como argumento y luego aplicar la lógica antes descripta para decidir siel proceso puede o no escribir en el �le dado, retornando 0 en caso a�rmativo o el número negativoconvenido en caso contrario.

El helper principal que asiste a �owx_�le_permission() para realizar esta lógica se denominacan_write() y su código es:

int can_write (struct flowx_security_struct *fxs, sc *sc)

{

struct flowx_security_struct *current_sibling_fxs;

int ret = 1;

/* Primero chequea que el proceso no tenga la misma clase de

* acceso que el archivo o que el archivo domine al proceso.

*/

if (!sc_compare(fxs->sc, sc))

goto out;

else if (!sc_dominate(sc, fxs->sc))

goto noaccess;

/* Verifica que la clase de acceso del proceso sea el supremo

* de las clases de acceso de sus s_hermanos.

*/

for_each_s_sibling(fxs, current_sibling_fxs) {

if (!sc_compare(current_sibling_fxs->sc, sc))

goto noaccess;

if (sc_dominate(sc, current_sibling_fxs->sc))

if (sc_compare(current_sibling_fxs->sc, fxs->sc) > 0)

goto noaccess;

}

goto out;

Page 103: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 101

noaccess:

ret = 0;

out:

return ret;

}

Al igual que en sys_read(), la llamada sys_write() debe ser modi�cada en el código del kernelpara que, en caso de que el hook devuelva el valor negativo elegido, retorne como si hubiese escritonormalmente el archivo objetivo. A continuación, puede observarse esta pequeña modi�cación:

ssize_t vfs_write(struct file *file, char __user *buf, size_t count, loff_t *pos)

{

ssize_t ret;

if (!(file->f_mode & FMODE_WRITE))

return -EBADF;

if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))

return -EINVAL;

if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))

return -EFAULT;

ret = rw_verify_area(WRITE, file, pos, count);

if (ret >= 0) {

count = ret;

ret = security_file_permission (file, MAY_WRITE);

//=========================== FLOWX ==============================

if (ret == -35)

return count;

//=========================== FLOWX ==============================

if (!ret) {

if (file->f_op->write)

ret = file->f_op->write(file, buf, count, pos);

else

ret = do_sync_write(file, buf, count, pos);

if (ret > 0) {

fsnotify_modify(file->f_path.dentry);

add_wchar(current, ret);

}

inc_syscw(current);

}

}

return ret;

}

5.7. Evaluación del primer prototipo

Luego de implementar el requerimiento y desarrollar algunos test sencillos para comprobar su fun-cionamiento, descubrimos que para lograr que la compatibilidad con los programas existentes, debíamosmodi�car algunos aspectos en las decisiones tomadas con respecto a qué deben devolver ambas llamadas

Page 104: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 5. LECTURA Y ESCRITURA DE ARCHIVOS 102

read y write cuando se niega el acceso al archivo. Para sys_read() lo más adecuado es que retorne 0,signi�cando que nada pudo leerse; mientras que para sys_write() devolver la cantidad requerida es lamejor solución.

La razón para tener que realizar esto, lo que implica necesariamente, una modi�cación en el kernel,es porque la mayoría de los programas chequea, luego de realizar una escritura o lectura, que el valordevuelto sea el correcto o esperado, �nalizando en caso negativo. Esto genera muchas fallas en laejecución que afectan notoriamente la usabilidad del programa y, peor aún, evidencian la presencia delmodelo de seguridad en el control de acceso, cosa que no queremos que suceda.

Otra falla que se encontró es que no debemos permitir que los procesos de alto nivel modi�quenlibremente los atributos normales del inodo que son exportados a espacio de usuario a través dela familia de llamadas al sistema stat. Permitirlo signi�caría la existencia de un canal encubierto,ya que un proceso podría comunicarle información a uno de menor nivel, por ejemplo, a través demodi�caciones constantes en el tiempo de último acceso o modi�cación de un inodo dado. La llamadasys_utime() sería la cómplice de este ataque.

La solución se centra en el hook inode_setattr, el cual imposibilitará que tal situación ocurra,devolviendo siempre éxito para para que la aplicación particular no se queje. Para lograr esto, mod-i�camos la función notify_change() en fs/attr.c, la cual representa un punto obligatorio que debeatravesar cualquier proceso para cambiar los atributos de un inodo particular.

Page 105: Flowx: Implementación de no interferencia en Linux

Capítulo 6

Compartir la entrada estándar entres-hermanos

El siguiente paso para lograr que Flowx sea un sistema usable es lograr que la entrada de bajo nivelsea compartida por todos loss-hermanos. Una vez que un proceso es duplicado creándose otro de mayornivel, ambos deberían actuar independientemente sin que el usuario tenga la menor idea de que ahorahay dos procesos corriendo el mismo código pero con diferentes clases. Luego, el usuario debería seguirinteractuando con un único proceso a través de la terminal. Pero aquí es donde surge el problema ya quepuede llegar un punto en el programa donde los dos s-hermanos requieran una entrada para continuarsus respectivas ejecuciones. Para obtener transparencia en la ejecución de s-hermanos, debemos lograrque una vez leída la entrada por uno, está sea distribuida entre los restantes. Sin embargo, tampocoes tan sencillo, ya que debemos esparcir esta distribución también entre todos los futuros familiares delos s-hermanos involucrados, como muestra la Figura 6.1. Para desarrollar este capítulo usaremos losconceptos estudiados en la sección 4.6.

Figura 6.1: Situación simple donde se debe compartir la entrada. El número representa la clase deacceso del proceso.

6.1. Analizando y diseñando el requerimiento

En esta sección analizaremos las diferentes posibilidades de implementar el requerimiento y mostraremosel diseño �nalmente implementado. En la sección 6.2 comentamos en detalle tal implementación.

103

Page 106: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 104

Previamente, en el momento de la creación de un s-hermano, la función copy_process() asignaba elnuevo proceso a un grupo y sesión dependiendo las banderas usadas para la clonación. Como nosotrosduplicamos el proceso con todas las banderas limpias, copy_process() pondrá el nuevo proceso en elmismo grupo que su creador, o sea su s-hermano, y le asignará la misma terminal que este:

.

.

.

#define thread_group_leader(p) (p == p->group_leader)

.

.

.

static struct task_struct *copy_process(***)

{

p->group_leader = p;

.

.

.

if (thread_group_leader(p)) {

p->signal->tty = current->signal->tty;

p->signal->pgrp = process_group(current);

set_signal_session(p->signal, process_session(current));

attach_pid(p, PIDTYPE_PGID, process_group(p));

attach_pid(p, PIDTYPE_SID, process_session(p));

.

.

.

}

.

.

.

}

Ya tenemos un dato útil para diseñar el requerimiento: todos los s-hermanos pertenecerán al mismogrupo. Esto implica el hecho de que cuando un s-hermano lea alguna entrada de la terminal, los otrosno dormirán ni se les enviará ninguna señal de parada. Por ende, a medida que cada hermano obtengael uso del CPU podrá ir leyendo de la terminal sin que esto afecte a los demás.

Algo que hay que tener en cuenta es que la entrada no puede ser compartida por todos los hermanosya que esto depende del nivel de la terminal de la cual se está queriendo leer. Momentáneamente, elnivel de acceso de todos los dispositivos es el menor posible. Sin embargo, esto cambiará en el futuro,ya que una asignación de una clase de acceso a los componentes que simbolizarían la conexión con elentorno es imperativa para el correcto funcionamiento del modelo. Sabiendo esto, debemos imponerque la entrada se comparta solo entre s-hermanos con nivel mayor o igual a la entrada de la terminal

La primera opción fue compartir la entrada por medio de un bu�er accesible por todos los s-hermanos, pero la pregunta era quién llenaría tal bu�er. En una primera instancia se pensó en crearun kernel thread que se encargue de administrarlo, pero surgían ciertas di�cultades:

Page 107: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 105

Este debía vivir en un bucle in�nito, ya que tendría que existir durante toda la vida del módulo.

Había que tener en cuenta que la entrada se debía compartir entre todos los s-hermanos de ungrupo dado, y no con todos los s-hermanos del sistema. El thread debería alojar un bu�er porcada grupo y manejar los diferentes accesos a éstos y quién los accedía.

La estructura tty_struct debía tener como grupo actual al grupo del kernel thread.

Según el item anterior, todos los grupos de s-hermanos tenían que pertenecer al mismo grupo deprocesos que el thread, dado que si no, la función job_control() los detendría inmediatamente alver que un grupo que no es el foreground quiere acceder a la terminal.

Un enfoque más intuitivo y simple es que cada grupo de s-hermanos maneje su propio bu�er deentrada. Aquí surge el problema de que a pesar de funcionar para los casos más triviales, el hechocompartir solo entre s-hermanos, no posibilitará que los hijos de éstos puedan hacerlo. Un ejemploclaro es el de la Figura 6.1 (en el inicio de la sección) donde los procesos Proc 0 y Proc 1 ya no gozaránde un procesamiento idéntico de la entrada.

Esta di�cultad nos lleva a diseñar el requerimiento teniendo en cuenta estos items:

La entrada debe ser compartida entre niveles, no únicamente entre s-hermanos.

Al visualizar la división de procesos con respecto a su nivel, hay que poder diferenciar entreaquellos procesos que pertenecen a la sesión actual y aquellos que no. Obviamente, no se pretendeque un proceso nivel 0 en la tty1 comparta su input con uno nivel 1 en la tty2.

Hay que pensar que en un momento dado puede haber varios procesos de un cierto nivel compar-tiendo la entrada con otros de nivel diferente, por ende la concurrencia surge como un aspecto asolucionar.

Para combatir estos puntos se decidió crear un bu�er por nivel por sesión de procesos e implementaruna lógica consistente para su uso y administración. Aquí yace la di�cultad de cómo controlar el accesoconcurrente al bu�er y cómo registrar qué debe hacer un hermano al momento de querer leer de laterminal, o sea, si hacerlo directamente de la tty u obtener los datos del bu�er. Una problema extraera el caso en que el proceso requiriese cierta cantidad de su entrada estándar y esa cantidad fuese�mixta�, o sea, parte desde el bu�er y lo restante desde la terminal. Por lo tanto, se decidió que soloun proceso por sesión controle el bu�er y que éste dé permiso o señale a los demás cuando haya nuevaentrada disponible. Como lo que se busca es una solución transparente para el usuario, o sea, que nose ponga en evidencia la creación de s-hermanos, lo más lógico es que el proceso controlador (jefe) seaaquel cuyo nivel sea el que tenga acceso a la escritura en la tty, o sea, que el proceso pueda escribir enel archivo especial asignado a la tty. Para esto, simplemente, se utiliza la misma función que usábamosen sys_write() para ver si un proceso puede o no escribir en un archivo. En este caso, el archivo será,por ejemplo, /dev/tty1. La idea es que todos los procesos restantes, que requieran información de dichatty, duerman hasta que el jefe haya procesado la entrada y los despierte.

Lo que resta es decidir el punto, durante la llamada read(), para administrar el bu�er y cómodistinguir entre procesos normales y aquellos que deben compartir su entrada con otros. Decidimosdesarrollar ambas tareas en la misma llamada sys_read(), mediante un pequeño truco implementadoen el hook �owx_�le_permission(), el cual consiste en lo siguiente:

Una vez que se pasó exitosamente el chequeo de si el proceso puede leer del archivo especial de latty y si current debe compartir el input, redireccionar el hook correspondiente al read() de estearchivo por un read nuestro.

Cuando sys_read() llame a �le->f_op->read(), invocará a nuestro read, el cual:

Page 108: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 106

• Restaurará inmediatamente �le->f_op->read() a su valor original.

• Chequeará si el proceso es el jefe de la sesión o no.

◦ Si no lo es y no hay nada en el bu�er por leer, dormirá. Si hay, leerá lo que pueda obtenerdel bu�er y si esto no satisface la cantidad requerida, dormirá también. Si satisface supedido, se almacenará lo leído en el bu�er de espacio de usuario pasado originalmentea la llamada sys_read().

◦ Si es el jefe, ejecutará �le->f_op->read() (ahora lee efectivamente de la tty), almacenarálo leído en el bu�er particular de cada nivel registrado y despertará a los procesosdormidos esperando una entrada. Aquí también el jefe deberá comprobar si el espaciodel bu�er se ha agotado o no.

Cabe notar que el bu�er de un nivel dado será alojado cuando un proceso de este nivel sea creado yserá destruido cuando no existan más procesos de dicha clase de acceso. Por lo tanto, debemos proveerun mecanismo para registrar y remover los bu�ers por nivel, para que el jefe sepa en cuáles almacenarinformación y en cuáles no.

6.2. Detalles de la Implementación

Para implementar el requerimiento del bu�er, consideremos que necesitamos:

Una estructura que represente a cada bu�er.

Una estructura que simbolice cada sesión.

Una estructura de cada proceso que describa la forma particular de acceso al bu�er.

6.2.1. El Bu�er

El bu�er de cada nivel estará representado por la estructura �owx_bu�er :

struct flowx_buffer {

struct kref ref;

__u32 level;

struct rw_semaphore *sem;

struct session_buffer *my_session;

wait_queue_head_t *wq;

struct list_head others;

int active;

int quit;

int count;

int flush_count;

unsigned long data[0];

};

El detalle de sus campos es el siguiente:

ref es un conteo de referencia para saber cuándo desalojar el bu�er. Al momento de crear uns-hermano de determinado nivel, se llama a la función �owx_bu�er_register() para registrar unnuevo integrante al bu�er del nivel. Si el bu�er no existe, entonces es creado; de lo contrario, setoma una referencia a éste (se incrementa el conteo). Cuando el s-hermano termina su ejecución,�owx_bu�er_unregister() es invocada, la cual decrementa el contador.

Page 109: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 107

El objeto kref es una utilidad que provee el kernel para manejar conteos de referencias y estáde�nido en include/linux/kref.h. Consta de una simple estructura que contiene un valor atómico(su modi�cación se realizará sin posibilidad de interrupciones en el medio) y de funciones paraincrementar o decrementar dicho valor. Para el primer caso existe la función kref_get() y parael segundo kref_put(). Esta última, además de recibir como argumento el kref a decrementar,recibe un puntero a una función del tipo

void (*release) (struct kref *kref)

Esta función será la invocada al momento de que el contador llegue a cero y, supuestamente,tiene que ser la encargada de liberar la memoria utilizada por el contenedor de la referencia. Ennuestro caso, dicha función se llama �owx_bu�er_free().

level indica el nivel de los procesos que utilizarán el bu�er especí�co.

active representa la cantidad de procesos activos que estén usando el bu�er. Llamaremos procesosactivos a aquellos que en un momento dado requieran información del bu�er. Estos se diferenciande aquellos procesos registrados en el bu�er pero que no lo están utilizando momentáneamente,debido, por ejemplo, a que están dormidos. Al igual que ref, este campo lo incrementa solo uns-hermano cuando requiera información del bu�er por primera vez y se decrementa al morir. Verademás el item sobre �ush_count.

Sin embargo, se podría argumentar lo siguiente: si active se incrementa cada vez que un s-hermanolee por primera vez del bu�er, y no por cada hijo de un s-hermano, entonces active no determinalos procesos activos totales del bu�er. Obviamente, esto es cierto, pero explicaremos que re�ejaexactamente el campo active.

Cuando un proceso crea un hijo tiene dos opciones, o poner al hijo en otro grupo y que éstesea detenido al momento de leer de la terminal, o esperar a que el hijo termine para que elpadre reanude su ejecución. Hay una tercera que es dejar que el padre y el hijo interactúencon la terminal al mismo tiempo, pero es demasiado caótico para que un programa funcioneadecuadamente. Observemos el ejemplo de la Figura 6.2.

Figura 6.2: Bash 0 y Bash 1 son s-hermanos. Bash 0 ejecuta un programa Proc el cual accede ainformación de nivel 1. Los números simbolizan el nivel de acceso de cada programa.

Page 110: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 108

En el caso que ilustra la �gura, Bash 0 y Bash 1 son s-hermanos, lo que implica que comparten laentrada. Bash 0 ejecuta un programa llamado Proc y Bash 1 pueden también ejecutar Proc perocon nivel 1. Notar que pudo haber sucedido que el comando que utilizó Bash 0 para ejecutar Procno haya sido válido para que Bash 1 también lo haga. Esto sucede, por ejemplo, cuando estoss-hermanos poseen directorios actuales con rutas diferentes y el comando en cuestión utiliza rutasrelativas para referenciar al archivo ejecutable del programa que se quiera invocar. Este caso esilustrado por la �gura 6.3. Además en ambas �guras se asume que Proc accederá a informaciónde nivel 1.

En ambas situaciones surgen dos posibilidades:

Bash 0 ejecuta a Proc en segundo plano:

Aquí los únicos procesos que requerirán entrada de la terminal son Bash 0 y Bash 1, dondeel único activo en el bu�er será este último. Ahora active tiene el valor 1 ya que ninguno delos s-hermanos creados por Proc pudo acceder al bu�er. Si hubiésemos incrementado activepor cada hijo de s-hermano, el campo tendría el valor 3 algo que no re�eja lo que realmentese tendría que representar esta variable.

Bash 0 ejecuta a Proc en primer plano:

Esta situación es análoga a la anterior, con la diferencia de que los procesos Proc 1 accedenal bu�er, de los cuales solo el s-hermano de Proc 0 incrementa active. Así active contieneel valor 2 que simboliza que dos procesos están actualmente consumiendo el bu�er: los dosProc 1 en la �gura 6.2 y Bash 1 y Proc 1 en la �gura 6.3.

Figura 6.3: Bash 0 y Bash 1 s-hermanos y Bash 1 no ejecuta Proc.

Otro lector también podría notar que el conteo de referencia tiene el mismo valor que la variableactive, y estaría en lo cierto, pero su diferencia radica en la instancia en la cual son incrementa-dos. ref se incrementará apenas un s-hermano se cree, mientras que active lo hará cuando ésteacceda por primera vez al bu�er. Si observamos nuevamente el caso en que Proc es ejecutado

Page 111: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 109

en background, como Proc 1 nunca va a tener un acceso directo a la terminal nosotros debemosdetenerlo al detectar que su grupo es diferente al grupo actual que controla la tty1. Si Proc 0generó un s-hermano antes de acceder a la terminal, entonces claramente ref sería igual a 2,mientras que active igual a 1.

others es la cabecera de una lista enlazada que contiene a todos los procesos, activos o no,que estén suscritos al bu�er. Los procesos estarán conectados a través de un campo particular,integrante de la estructura correspondiente a cada proceso, el cual se discutirá en breve.

count indica la cantidad de caracteres que tiene almacenado el bu�er en un momento dado. Estecampo es muy útil para determinar cuándo no hay más espacio de almacenamiento o cuántopuede leer un proceso determinado.

quit es una bandera que una vez activa le indica a todos los suscriptores de una �owx_bu�er quedeben dejar de esperar por una entrada.

La razón principal de esta variable es que hay veces en que queremos matar a un proceso poralgún motivo particular. Para lograr esto, debemos enviar una señal SIGKILL, que puede serenviada por el usuario (comando kill) o por algún proceso del kernel (send_sig()). Las señalesson procesadas en el momento en que el proceso retorna al espacio de usuario, pero si ésteduerme inde�nidamente en espacio kernel, la señal nunca será entregada. Para que no ocurra elcaso en que el jefe del bu�er muera y algún suscriptor duerma esperando entrada que nunca leserá enviada y así, tener un proceso huérfano y eterno en el sistema, la variable quit debe serinicializada para que dicho suscriptor sepa que no debe esperar más una entrada.

�ush_count es una variable de suma importancia para vaciar el bu�er con cierta periodicidad yevitar que se llene frecuentemente. Básicamente, cada vez que un proceso �no jefe� determine quesu entrada es lo último que puede obtener del bu�er, incrementará en uno el campo �ush_countde éste. Luego se chequeará el valor de está variable con el de active. Que sean iguales signi�caque todos los procesos activos en el bu�er requirieron un �ush y, por ende, es seguro reiniciarlo;lo que implica poner en 0 al campo count y avisar a todos los suscriptores de que sea ha vaciado(gracias a la lista others). Sin embargo, hay que tener especial cuidado con el manejo de estecampo ya que un proceso puede incrementarlo varias veces producto de situaciones �jefe escribe,esclavo lee�. Supongamos que el jefe ingresa al bu�er entradas de a un caracter. Cada vez queun suscriptor lea uno, estará automáticamente satisfecho, un �ush será pedido por cada una deestas lecturas y �ush_count crecerá rápidamente. Debido a este problema, cada proceso debesaber si ya ha pedido o no un �ush. Si ya lo pidió y requiere otro, simplemente no hace nada. Si,en cambio, pidió uno y ahora ve que no está satisfecho decrementa �ush_count. De esta manera,la única forma en que �ush_count sea igual a active es que estén todos satisfechos al mismotiempo, situación que ocurre con frecuencia y evita que el bu�er se llene demasiado.

wq es un puntero a un cola de espera, las cuales se de�nen en include/linux/wait.h. Esta colaserá usada para que los procesos que requieran entrada y todavía no esté disponible, duermanen ella.

sem es un puntero a un semáforo de escritura y lectura, de�nido en include/asm-i386/rwsem.h.Su existencia se debe a que debemos controlar el acceso concurrente al bu�er y a las variablescomunes a todos sus suscriptores. Usamos este tipo de semáforo en vez de uno normal ya que solohabrá un escritor y muchos lectores. Este mecanismo de sincronización permite que haya muchoslectores al mismo tiempo, pero todos se bloquean al momento en que un escritor quiere acceder

1Esta es una medida provisoria que se toma para que ambos s-hermanos se comporten de la misma manera ante unaseñal SIGTSTP provocada por la terminal. Trabajo futuro tratará las acciones a tomar con respecto a las señales entres-hermanos, o sea, si deben ser compartidas o no.

Page 112: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 110

al recurso compartido. Permitir lecturas concurrentes de nuestro bu�er mejora la performancede nuestro módulo.

my_session es un puntero a la estructura de datos común a todos los procesos de la misma sesiónque estén compartiendo la entrada. Esta estructura se denomina session_bu�er y se detalla enla sección 6.2.2.

6.2.2. Las sesiones

Como mencionamos en la sección 6.1, debemos diferenciar entre las diferentes sesiones en las que sepueden encontrar los procesos con distintos niveles en el sistema. La estructura session_bu�er surgepara facilitar este requerimiento y, simplemente, consta de una colección de �owx_bu�ers.

struct session_buffer {

struct kref ref;

int buffers[LEVEL_SIZE];

struct flowx_buffer *all_buffers[MAX_LEVELS];

};

Al igual que en la estructura anterior, tenemos un conteo de referencias para controlar la existenciadel bu�er por sesión, al cual las funciones entering_session() y leaving_session() incrementan y decre-mentan, respectivamente. Cuando el conteo llegue a cero, remove_session() liberará la memoria de lasession_bu�er referenciada. Se tuvo especial cuidado en el código para que nunca una session_bu�ersea liberada antes de que lo sean todos los �owx_bu�ers que contenga.

El campo bu�ers es un campo de bits que representa los bu�ers actualmente alojados en la sesión.De esta forma, los chequeos de si existe un bu�er en particular o si una sesión tiene procesos con inputcompartido, se hacen a velocidades mayores.

6.2.3. Asociando procesos con bu�ers

Para �nalizar, describimos la estructura correspondiente a cada proceso, que contiene informaciónvital para saber la forma en que cierto proceso debe manejar la entrada compartida. La estructuratask_input_bu�er será parte de la �owx_security_struct (discutida en la sección 5.2) a través de sunuevo campo input y, por ende, será alojada al mismo tiempo en que un proceso sea creado.

struct task_input_buffer {

struct session_buffer *sbuf;

struct list_head others;

unsigned short my_pos;

unsigned char flags;

};

Sus campos se detallan a continuación:

sbuf contiene un puntero a la session_bu�er a la cual pertenece el proceso. Las session_bu�ersson alojadas cuando se crea por primera vez un s-hermano de cierto nivel lo que signi�ca quela entrada de la terminal debe empezar a compartirse. En ese momento se inicializa este campo,pero además hay que inicializar el mismo campo de la task_input_struct del líder de la sesión.De esta forma, la próxima vez que un proceso requiera una referencia a su session_bu�er podráconsultar directamente en la task_input_struct de su líder.

Page 113: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 111

others es una lista a través de la cual se enlazarán los procesos del mismo nivel de la mismasesión. Cuando describimos el campo others de la estructura �owx_bu�er mencionamos que éstesimplemente era la cabecera de la lista y que los procesos estarían realmente enlazados por otravariable. Esta es la variable en cuestión.

my_pos indica la posición dentro del bu�er a partir de la cual el proceso debe empezar a leer.Esta posición es relativa a cada proceso y será reiniciada únicamente (se le asigna el valor 0)cuando el bu�er sea vaciado.

�ags en un campo de 8 bits los cuales representan:

• UNREG (bit 1). El proceso todavía no ha sido registrado en su bu�er correspondiente.Registrar el proceso implica: agregarlo a la lista de suscriptores del bu�er, inicializar my_pose inicializar el campo sbuf. Con respecto al campo my_pos, éste debe ser inicializado para serla posición siguiente, en el bu�er, a la que le correspondería a su padre. De la misma manera,cuando el proceso muera, deberá indicarle a su progenitor a partir de dónde continuarleyendo.

• SIMPLE_TASK (bit 2). El proceso es hijo de un s-hermano. Los procesos hijos de s-hermanos no toman referencias al bu�er, pero sí a la sesión. Tampoco incrementan el campoactive en su primer ingreso al repositorio compartido.

• S_SIBLING (bit 3). El task es un s-hermano de otro.

• CANON (bit 4). El proceso debe procesar la entrada del bu�er como si estuviera en unaterminal canónica. Este aspecto se detallará más adelante.

• FLUSH (bit 5). El proceso requirió un �ush del bu�er.

• Los bits 6, 7 y 8 no se utilizan.

6.2.4. La interfaz para task_input_bu�er

Además de estas estructuras, se de�nieron diferentes funciones para el manejo del bu�er. La funciónencargada de reemplazar �le->f_op->read será �owx_share_input(), la cual hará su tarea siempre ycuando se deba compartir el input, cuyo chequeo es desarrollado por la función is_sharing_input(), ycuando el archivo al que se quiera acceder, corresponda a la entrada estándar del proceso y sea unaterminal.

La función envoltorio de �le->f_op->read será �owx_bu�er_read(). Esta función debe chequear sicurrent es o no el jefe encargado de abastecer a los bu�ers alojados en su sesión para que los procesos,que no puedan acceder directamente a la terminal, puedan consumir su entrada compartida de susrespectivos repositorios. Esta función toma caminos diferentes en caso de estar en presencia o no deljefe:

Si se determina que el proceso actual no es el jefe, se llama a read_from_bu�er(), lugar dondese considera que el proceso necesita información compartida y, por ende, se inicializan, en lasestructuras mencionadas, todas las variables necesarias para realizar esta tarea. Es también aquí,donde los procesos sin más entrada disponible dormirán y donde se analizará de qué forma proveerla información requerida a cada suscriptor. Esto ocurre porque existen dos tipos de lecturas desdela terminal, la canónica y la no-canónica. Como explicamos en la sección 4.6.5. Lo bueno de laforma canónica de procesar la entrada es que quien edita la línea es el jefe, mientras que losdemás recibirán solo la versión de�nitiva de ésta.

Para determinar la forma en que el bu�er debe emular a la terminal es que existe el bit CANONen las banderas del campo �ags de la task_input_bu�er. Si la entrada se debe proveer de formacanónica, la función input_available() retornará la porción del bu�er comprendida entre la posi-ción relativa del proceso solicitante (campo my_pos) y el primer retorno de carro encontrado. Si

Page 114: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 112

se debe emular la forma no canónica, entonces input_available() devolverá de a un caracter a lavez, empezando en my_pos.

Si current es el proceso que debe manejar el bu�er, entonces lee de la terminal a través de lallamada read() del driver tty y guarda lo leído en cada bu�er de su sesión, despertando a cadaproceso dormido esperando entrada antes de retornar.

Anteriormente, se dijo que el jefe era aquel proceso con el mismo nivel de la terminal. De estamanera, solo un proceso controlaría la con�guración de esta e invocaría sus funciones. Esto produceque, más allá de que todos los procesos registrados para compartir entrada tengan diferente pgrp,ninguno se bloqueará ya que solo uno pasará por la función job_control() de la tty correspondiente.Sin embargo, aparece un problema que debemos resolver. Supongamos que estamos en el caso de laFigura 6.2 y que la terminal tiene una clase de acceso 1. Si nos basamos únicamente en la funcióncan_write() para elegir al jefe, entonces tendríamos dos candidatos: los dos Proc de nivel 1. Estoproduciría resultados desastrosos a la hora de compartir la entrada porque el jefe cambiaría todo eltiempo. Pero, ¾quién debería ser el jefe en este caso? Para comprender la respuesta pensemos en el árbolque generan estos programas al duplicarse en s-hermanos. En la �gura 6.4 podemos observar como,si seccionamos verticalmente este árbol con respecto a los s-hermanos más �antiguos�, obtenemos tresniveles principales de input compartido. La clase de acceso de cada sección es igual a la clase acceso dela raíz de cada subárbol. Con una terminal 1, los dos Proc 1 tendrían derecho a ser el jefe, pero comoqueremos que solo uno controle la tty, se la daremos a aquel que pertenezca a la sección con la mismaclase de acceso que la terminal. En nuestro caso, Proc 1 hijo, de Bash 1. Este tendrá que rellenar:

El bu�er de nivel 0, para Proc 0.

El bu�er de nivel 1, para Proc 1 de Bash 0.

El bu�er de nivel 2, para Proc 2 tanto de Bash 1 como de Bash 2.

Page 115: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 113

Figura 6.4: Tres niveles actuales con terminal 1

Las funciones chief_relative() y can_write() determinan al jefe:

int chief_relative(struct task_struct *task, sc *sc)

{

struct task_struct *relative, *temp;

struct flowx_security_struct *fxs;

temp = relative = task;

while (temp->parent) {

if (!(fxs = temp->parent->security))

break;

if (IS_S_SIBLING(fxs->input))

relative = temp->parent;

temp = temp->parent;

}

fxs = relative->security;

return can_write(fxs, sc);

}

Otro problema que surge cuando la terminal no es 0 y se da un caso similar a la de la �gura 6.2,es que los dos programas con la clase de acceso de la terminal (en este caso 1) podrán imprimir susalida en pantalla. En este caso, can_write() determina que la escritura se puede llevar a cabo, ya que

Page 116: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 114

ambos procesos tienen la misma clase de acceso que el archivo tty. A pesar de que no fue diseñadapara resolver esta situación, chief_relative() auxilia perfectamente a �owx_�le_permission() a quelo mencionado no ocurra. Por lo tanto, modi�camos �owx_�le_permission() para que en la seccióncorrespondiente a sys_write() se contemple este problema, invocando a chief_relative() cuando seanecesario.

6.2.5. Simultaneidad en la con�guración del controlador tty

Con todo lo mencionado, el requerimiento de compartir la entrada de bajo nivel funciona correc-tamente para cada lecto-escritura sobre la terminal. Sin embargo, los procesos no solo leen y escribenen la terminal sino que además con�guran varios parámetros que in�uyen en el comportamiento delcontrolador de la tty. La con�guración de estos parámetros se hace a través de la llamada sys_ioctl().

Como en Flowx la tty es un recurso compartido entre s-hermanos, puede ocurrir que más de unoquiera cambiar el modo de la terminal a no canónica y sin eco. El s-hermano que lo intente porsegunda vez, hará exactamente lo mismo, pero con la diferencia de que salvará por defecto los modosde la terminal asignados anteriormente. Esto ocurrirá casi siempre, ya que el primer s-hermano norestaurará la terminal a los valores por defecto guardados antes de recon�gurarla hasta que no recibaentrada, la cual no estará disponible hasta que el jefe la procese. Este problema surge particularmentecuando Bash se duplica porque tiene como característica proveer sus propias facilidades de ediciónde línea de forma tal que a todo read() que requiera un carácter de teclado, le siga un write() paraimprimirlo en pantalla (una especie de eco a nivel de aplicación). Para comprender más en detalle estasituación analicemos el siguiente código:

void reset_input_mode (void)

{

tcsetattr (STDIN_FILENO, TCSANOW, &saved_attributes);

}

void set_input_mode (void)

{

struct termios tattr;

char *name;

/* Se asegura de que stdin sea una terminal. */

if (!isatty (STDIN_FILENO)) {

fprintf (stderr, "No es un terminal.\n");

exit (EXIT_FAILURE);

}

/* Guarda los atributos de la terminal para poder restaurarlos

* más adelante.

*/

tcgetattr (STDIN_FILENO, &saved_attributes);

atexit (reset_input_mode);

/* Setea modos de la terminal arbitrarios. */

tcgetattr (STDIN_FILENO, &tattr);

tattr.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON y ECHO. */

tattr.c_cc[VMIN] = 1;

tattr.c_cc[VTIME] = 0;

tcsetattr (STDIN_FILENO, TCSAFLUSH, &tattr);

Page 117: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 115

}

int main (int argc, char **argv, char **envp)

{

char c;

int file;

/* dirprueba es un directorio de alto nivel, por lo que veremos

* en la sección siguiente este open genera un s-hermano.

*/

file = open("dirprueba/secreto", O_RDWR);

if (file > 0)

printf("Soy s-hermano\n");

else

printf("Soy el jefe\n");

set_input_mode ();

while (1) {

read (STDIN_FILENO, &c, 1);

if (c == '\004') /* Ctrl-d */

break;

else

putchar (c);

}

return EXIT_SUCCESS;

}

El programa es una simple imitación de lo que realiza Bash a la hora de acceder a la terminal.Normalmente el código ejecuta la siguiente secuencia de llamadas a ioctl() cuando invoca a tcgetattr()o tcsetattr():

En set_input_mode()

tcgetattr() → ioctl(0, ..., B9600 opost isig icanon echo):

Guarda los modos actuales de la terminal: que es canónica y con eco.

tcsetattr() → ioctl(0, ..., B9600 opost isig -icanon -echo):

Con�gura la terminal para que ahora sea no canónica y sin eco.

En reset_input_mode()

tcsetattr() → ioctl(0, ..., B9600 opost isig icanon echo):

Resetea los modos de la terminal como estaban originalmente, gracias a los salvados en laprimer llamada a tcgetattr().

Por el contrario, cuando está activo el módulo Flowx :

En set_input_mode()

Page 118: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 116

tcgetattr() → ioctl(0, ..., B9600 opost -isig -icanon echo):

Guarda los modos actuales de la terminal que misteriosamente, ya están en no canónico ysin eco.

tcsetattr() → ioctl(0, ..., B9600 opost isig -icanon -echo):

Con�gura la terminal para que ahora sea no canónica y sin eco, o sea, no hace nada nuevo.

En reset_input_mode()

tcsetattr() → ioctl(0, ..., B9600 opost isig icanon echo):

Resetea los modos de la terminal como estaban originalmente, pero como tcgetattr() obtuvomodos ya corruptos, deja mal con�gurada la terminal.

La solución para este inconveniente yace en intervenir la llamada sys_ioctl(). Siguiendo nuestradecisión sobre quién debe manejar la terminal determinamos que solo el jefe pueda emitir llamadasioctl() a su tty asociada. Para esto, usamos el hook security_�le_ioctl() para implementar nuestrafunción �owx_�le_ioctl(), la cual:

Comprueba que el archivo argumento sea una tty.

Que el proceso invocador esté compartiendo la entrada.

Invoca a la función �owx_bu�er_ioctl(), la cual determina si se puede realizar la llamada o no.

Sin embargo, queremos que los procesos que usan el bu�er compartido puedan procesar su con-tenido como tendrían que haberlo hecho en caso de que todo funcionara normalmente. Por lo tanto,�owx_bu�er_ioctl() también se encarga de determinar si la llamada ioctl() estaba destinada a cambiarel modo de lectura de la tty y activa o no en el bit CANON en el campo �ags de la task_input_bu�er,para luego poder emular este comportamiento. Además, para que un proceso no jefe no sepa que sullamada ioctl() fue rechazada, se hizó una pequeña modi�cación en fs/ioctl.c de forma tal que si elproceso no tiene permisos para realizar la llamada, igualmente se le retorne éxito.

6.2.6. Acceso no bloqueante a la terminal

Un último aspecto a analizar es la manera en que muchas aplicaciones acceden a la terminalbuscando no dormir cuando la entrada no se encuentre disponible. Muchas veces un programa quiereesperar eventos desde varios dispositivos. Algunos de estos eventos pueden ser la posibilidad de lecturao escritura sobre el dispositivo o cuando algún tipo de error ha ocurrido. Normalmente, el programaemite pedidos para ser informado de estas situaciones, activa una especie de temporizador y realizatareas de mantenimiento o diagnóstico mientras espera. Esto se logra gracias a la interfaz poll queproveen todos los controladores de dispositivos en el sistema Linux. Esta interfaz es muy simple y elkernel subyacente brinda casi toda la infraestructura necesaria.

En el caso de la tty, ésta ofrece en su estructura �le_operations la función tty_poll(), que a su vezinvoca a la rutina normal_poll() de su disciplina de línea asociada. normal_poll() chequea, principal-mente, si alguno de los bu�ers de entrada contiene información disponible y devuelve una máscara conalguno de los siguientes bits activados:

POLLIN: Se activa cuando datos pueden ser leídos de la tty sin bloquear (dormir) a nadie.

POLLPRI: Se activa cuando datos de alta prioridad pueden ser leídos sin bloquear.

POLLHUP: Se activa cuando el dispositivo ve un ��n de línea�.

POLLERR: Se activa cuando una condición de error ha ocurrido en el dispositivo.

Page 119: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 6. COMPARTIR LA ENTRADA ESTÁNDAR ENTRE S-HERMANOS 117

POLLOUT: Se activa cuando datos pueden ser escritos en la terminal sin bloquear.

La llamada al sistema sys_select(), en fs/select.c, permite a un programa monitorear múltiplesdescriptores de archivos, esperando a que uno o más de estos descriptores estén listos para una operaciónde E/S sin bloquear al proceso invocador. select usa la interfaz poll mencionada y es muy usada,especialmente, por editores de texto.

Para lograr compatibilidad con los programas que invoquen recurrentemente a esta syscall, debemosproveer nuestra propia noción de select cuando el proceso esté compartiendo la entrada y no accedadirectamente a la terminal. Para esto agregamos el hook �le_select al framework LSM, lo incluimosen la función do_select() y modi�camos todos los archivos necesarios para agregar un hook nuevo. Elprototipo de la función es:

int (*file_select) (int nfds, fd_set_bits *fds);

donde fd_set_bits de�nida en include/linux/poll.h es:

typedef struct {

unsigned long *in, *out, *ex;

unsigned long *res_in, *res_out, *res_ex;

} fd_set_bits;

donde in, out y ex son mapas de bits que indican los descriptores de archivos a analizar para laentrada, salida y excepciones, respectivamente. res_in, res_out y res_ex son mapas de bits asociadosa los primeros que indican, al �nal de sys_select(), cuáles descriptores están listos para la operaciónsin que haya bloqueo.

Particularmente, �owx_�le_select() solo auditará al descriptor 0 (entrada estándar) y si éste estáasociado a la tty. Si el proceso es el jefe, no hará nada. Caso contrario, chequeará, a través de la función�owx_bu�er_poll(), ls disponibilidad de información en el bu�er compartido de su nivel, devolviendores_in con�gurado con el fd 0 (res_in = (0 << 1)), en caso de que haya información almacenada queel proceso pueda leer.

6.2.7. Limitaciones

Para concluir con la sección, cabe destacar que aquí solo se estará compartiendo el input provenientede la entrada estándar de un proceso y, siempre y cuando, tal entrada provenga de la terminal asociada.Hay otras formas de que un proceso obtenga información de su entorno. Entre los mecanismos másconocidos se encuentran los sockets, los pipes, los mensajes, las FIFOS o los archivos regulares. Estosúltimos no son un problema, ya que ambos s-hermanos pueden abrir el archivo y la información estaráallí para su utilización. El inconveniente surge cuando la entrada es �borrada� una vez consumida. Estees el caso de la tty y es lo que tratamos de solucionar en esta sección. Los sockets y pipes tambiénposeen esta característica y la implementación sería más completa si los tuviésemos en cuenta. Sinembargo, tratar este tema requiere mucha más dedicación y conlleva una di�cultad que desborda elobjetivo de este trabajo, el cual es proveer un prototipo simple que siente las bases para un módulo deseguridad robusto y portable.

Page 120: Flowx: Implementación de no interferencia en Linux

Capítulo 7

Intervenciones a más llamadas al sistema

Las modi�caciones anteriores a las llamadas sys_read() y sys_write() sentaron las bases de có-mo debía procederse cuando un proceso no tenía permisos sobre un objeto dado. Particularmente,sys_read() nos introdujo en la noción de s-hermano y, posteriormente, en la entrada compartida. Sinembargo, éstas no son las únicas llamadas que deben intervenirse con el �n de implementar el modelo.

Hasta ahora, solo vimos que un proceso de bajo nivel no tiene derecho a acceder información de altonivel, por lo que éste es duplicado para que su hermano pueda acceder a dicha información. Pero, ¾quésucede cuándo el inodo de bajo nivel no es un archivo regular sino que es un directorio? Si seguimoscon nuestra línea de pensamiento sería lógico que cualquier usuario pueda saber la existencia de estedirectorio, pero no, de lo que contiene.

Para lograr esto, debemos usar más hooks LSM en diferentes llamadas al sistema. Entre ellas seencuentran:

sys_open(): Abre un archivo o lo crea si éste no existiese.

sys_creat(): Crea un nuevo archivo.

sys_getdents(): Lee un archivo correspondiente a un directorio para obtener sus componentes.

sys_mkdir(): Crea un nuevo directorio.

sys_mknod(): Crea un nuevo archivo especial.

sys_link(): Crea un enlace duro a un archivo dado.

sys_symlink(): Crea un enlace simbólico a un archivo dado.

sys_rmdir(): Borra un directorio.

sys_unlink() Borra un archivo.

sys_rename(): Renombra un archivo o directorio.

sys_stat(): Muestra el status de un archivo.

El resto del capítulo, repasará las decisiones de diseño, los problemas encontrados, las implementa-ciones y las características del kernel necesarias para entender las intervenciones realizadas sobre estasllamadas al sistema.

118

Page 121: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 7. INTERVENCIONES A MÁS LLAMADAS AL SISTEMA 119

7.1. Generalización del modelo Flowx

En esta sección se detalla una especi�cación para cada una de las nuevas llamadas a interveniren función de los �ujos de información que cada una produce, mostrando en qué puntos debemosintervenir, creando s-hermanos cuando sea neceario. A continuación se detallan los requerimientos quedebe cumplir cada llamada a la hora de invocarse.

Sea f un archivo o directorio:

Stat(f)

Si sccurrent ≺ scpadre(f) entonces:

• Si hay algún s-hermano sh con scsh = scpadre(f) → Permiso Denegado.

• Caso contrario, crear un s-hermano sh con scsh = scpadre(f)

◦ El s-hermano deberá volver a ejecutar sys_stat() exactamente como su hermano lohizo.

◦ Igualmente retornar éxito pero sin realizar el resto de la llamada.

Si no, retornar lo usual, o sea, la llamada prosigue normalmente.

Open(f)

Si sccurrent ≺ scpadre(f) entonces:

• Si hay algún s-hermano sh con scsh = scpadre(f) → Permiso Denegado.

• Caso contrario, crear un s-hermano sh con scsh = scpadre(f)

◦ El s-hermano deberá volver a ejecutar sys_open() exactamente como su hermanolo hizo.

◦ Igualmente retornar éxito pero sin realizar el resto de la llamada.

Si no, retornar lo usual, o sea, la llamada prosigue normalmente.

Create(f)

Si sccurrent 6= scpadre(f) entonces:

• Si hay algún s-hermano sh con scsh = scpadre(f) → Permiso Denegado.

• Caso contrario, crear un s-hermano sh con scsh = scpadre(f)

◦ El s-hermano deberá volver a ejecutar sys_create() exactamente como su hermanolo hizo.

◦ Igualmente retornar éxito pero sin realizar el resto de la llamada.

Si no, retornar lo usual y poner scf = sccurrent.

Getdents(f)

Si sccurrent ≺ scf entonces:

• Si hay algún s-hermano sh con scsh = scf → Permiso Denegado.

• Caso contrario, crear un s-hermano sh con scsh = scf

◦ El s-hermano deberá volver a ejecutar sys_getdents() exactamente como su her-mano lo hizo.

◦ Igualmente retornar éxito pero sin realizar el resto de la llamada.

Si no, retornar lo usual, o sea, la llamada prosigue normalmente.

Page 122: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 7. INTERVENCIONES A MÁS LLAMADAS AL SISTEMA 120

mkdir(), rename(), link(), symlink(), rmdir() y unlink() tienen exactamente el mismo requerimientoque create().

Como puede verse, las comprobaciones se hacen entre el proceso y el padre del archivo al cuál sele quiere realizar la operación, o sea, el directorio que lo contiene. De esta forma, nos aseguramos queningún software maligno pueda obtener información al querer acceder a un directorio de alto nivel, nique pueda comunicar datos secretos a procesos de bajo nivel.

En todas las especi�caciones el hecho de no permitir acceso, también signi�ca el retorno llamadaexitosa pero sin realizar nada. Para que el proceso pueda devolver éxito aún cuando no tenga permisospara realizar las llamadas, realizaremos pequeñas modi�caciones en el kernel. Esta modi�caciones sonsimilares a las hechas en sys_read() y sys_write() en las secciones 5.5 y 5.6 respectivamente. Esto sehace para que el s-hermano de bajo nivel que intenta acceder directorios secretos o, aquellos de altonivel que intenten crear sobre directorios con clase de acceso más baja, no detecten estas restriccionesque les imponemos y, por ende, sigan ejecutándose normalmente.

Lo ideal sería que los archivos con�denciales del sistema este guardados en directorios correspon-dientes a su nivel. Sin embargo, esto puede que no suceda, ya que un usuario de alto nivel podríacrear desde cero un archivo con�dencial en carpetas públicas. Gracias a los controles impuestos porsys_read() y sys_write(), la con�dencialidad del archivo quedará intacta.

7.2. Diseño de los ganchos LSM para este grupo de llamadas

Si observamos la sección 7.1 podemos notar que existe una mínima diferencia entre las llamadasopen() y stat() y las análogas a creat(). Las primeras buscan la supremacía de la clase de acceso delproceso frente a la del padre del archivo o directorio a tratar, mientras que las segundas buscan unaigualdad entre dichas clases.

Por otro lado, muchas de las llamadas al sistema de la segunda clase tienen su propio hook LSM:

sys_creat() ; security_inode_creat()

sys_mkdir() ; security_inode_mkdir()

sys_mknod() ; security_inode_mknod()

sys_rmdir() ; security_inode_rmdir()

sys_link() ; security_inode_link()

sys_unlink() ; security_inode_unlink()

sys_symlink() ; security_inode_symlink()

En tanto que sys_open() y sys_stat() no tienen hooks de�nidos pero dado que ambas utilizanel recorrido de caminos de directorios explicado en la sección 4.7 podemos usar security_inode_-permission() para nuestro objetivo. sys_getdents() invoca a vfs_readdir() la cual está controlada porsecurity_�le_permission() (recordemos que es el interventor principal de la llamadas read() y write()).

En consecuencia decidimos que haya dos funciones encargadas de implementar toda la lógica acercade cómo el módulo debe comportarse en estas situaciones: �owx_inode_permission() y �owx_check_-permission() descriptas en la sección 7.3.

Como se puede observar, con estos hooks nuevos más �owx_�le_permission() tenemos tres fun-ciones que crean s-hermanos. Por lo tanto, decidimos mover el código que duplica el proceso actualal helper create_new_s_sibling(), la cual se encarga de todo lo inherente al s-hermano, mientrasque los hooks restantes realizarán tareas de comparación entre clases de acceso e invocarán a cre-ate_new_s_sibling() cuando sea necesario.

Page 123: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 7. INTERVENCIONES A MÁS LLAMADAS AL SISTEMA 121

7.3. �owx_�le_permission() y �owx_check_permission()

La especi�cación de estas funciones es la siguiente:

�owx_inode_permission(struct inode *inode, int mask, struct nameidata *nd)

Implementa el hook security_inode_permission(), interviniendo sólo si se quiere acceder al archi-vo para ser ejecutado1 (mask & MAY_EXEC ). Si el acceso no está permitido (current no dominainode):

Si no hay s-hermano con la misma clase, lo crea.

Aloja memoria para el bu�er compartido.

Retorna −35. Este es un valor elegido al azar de entre todos aquellos valores de errno queno se devuelven en estos tipos de llamadas. La idea es que después, cada syscall, chequee elvalor devuelto por el hook de seguridad y si es −35, retorne como si todo hubiera funcionadoperfectamente.

Esta función será invocada indirectamente por link_path_walk() antes de resolver otra parte dela ruta de un archivo.

�owx_check_permission(struct inode *parent, struct dentry *son)

Será invocada por todas las implementaciones Flowx de los hooks listados arriba. Su función esmuy similar a la de �owx_inode_permission(), salvo que actúa siempre ante una desigualdad delas clases de acceso. Es más restrictiva. Otra característica que distingue a esta rutina es que sellamará una vez que el recorrido del camino haya resuelto el nombre del archivo. Por ende, si unproceso de bajo nivel quiere crear un archivo en una ruta donde algún componente sea de altonivel, el s-hermano se creará al momento del recorrido y no por �owx_check_permission().

Para la mayoría de las llamadas descriptas en esta capítulo, el requerimiento de devolver éxitoaunque no se otorgue permiso para ejecutar la llamada es relativamente sencillo: solo se deben agregaralgunas líneas en el código del kernel detectando el valor de retorno del hook correspondiente o el valorde retorno del recorrido del camino para el caso de open() y stat, y en caso de tener el valor −35,retornar inmediatamente de la llamada al sistema con un valor que simbolice el éxito de la misma.Sin embargo, para poder engañar a un proceso de bajo nivel cuando quiere abrir un archivo ubicadoen un directorio de nivel superior, lamentablemente, no basta con el mecanismo mencionado. Esto sedebe a que el éxito en una llama sys_open() viene acompañado con la devolución de un descriptor dearchivo válido para el nuevo archivo abierto. Engañar al proceso sin permisos signi�cará reservar unnuevo descriptor pero que referencie a un archivo �basura� donde toda escritura o lectura sobre él notenga efecto alguno. Si no asociamos un archivo válido sobre el fd (�le descriptor) particular, futurasllamadas a read o write con tal descriptor, provocarán fallos irremediables en el programa.

El mejor candidato para el archivo basura es /dev/null. Este archivo es un nodo de dispositivo, loscuales serán explicados con más detalles en el siguiente capítulo. Lo importante aquí es saber que estenodo sirve para acceder al dispositivo null, el cual descarta toda escritura sobre él y devuelve end of�le en cada lectura. Sin embargo, para que /dev/null sea 100% apto para ser útil para resolver esteproblema, debería ofrecer una función de sincronización entre memoria y disco, que todos los archivosregulares ofrecen. Esta función está referenciada en la estructura �le_operations que contiene cadaarchivo de Linux (include/linux/fs.h) y se llama:

int (*fsync) (struct file *, struct dentry *, int datasync);

1En Linux que un directorio pueda ser recorrido durante la operación de recorrer el camino es lo mismo que éste seaejecutable

Page 124: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 7. INTERVENCIONES A MÁS LLAMADAS AL SISTEMA 122

Como /dev/null no provee esta función, nosotros debimos implementarla para completar el engaño.En drivers/char/mem.c, creamos esta función y la ubicamos en la �le_operations de null :

.

.

.

//=========================== FLOWX ===============================

static int fsync_null(struct file *file, struct dentry *dentry, int datasync)

{

return 0

}

//=========================== FLOWX ===============================

static ssize_t read_null(struct file * file, char __user * buf,

size_t count, loff_t *ppos)

{

return 0;

}

static ssize_t write_null(struct file * file, const char __user * buf,

size_t count, loff_t *ppos)

{

return count;

}

.

.

.

static const struct file_operations null_fops = {

.llseek = null_lseek,

.read = read_null,

.write = write_null,

.splice_write = splice_write_null,

//=========================== FLOWX ===============================

.fsync = fsync_null,

//=========================== FLOWX ===============================

};

.

.

.

fsync() es invocada normalmente desde la llamada sys_fsync() por procesos que quieren asegurarseque un archivo que recientemente modi�caron sincronice. fsync() no retorna hasta que la sincronizaciónse haya completado.

Entonces, do_sys_open() en fs/open.c queda como sigue:

long do_sys_open(int dfd, const char __user *filename, int flags, int mode)

{

char *tmp = getname(filename); /* Obtiene la string 'filename'

de user space. */

int fd = PTR_ERR(tmp);

Page 125: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 7. INTERVENCIONES A MÁS LLAMADAS AL SISTEMA 123

if (!IS_ERR(tmp)) {

fd = get_unused_fd(); /* Solicita el siguiente fd

libre del proceso. */

if (fd >= 0) {

/* Realiza el path walk. */

struct file *f = do_filp_open(dfd, tmp, flags, mode);

//=========================== FLOWX =======================

/* Significa que no puede abrir el archivo porque

* flowx no se lo permitió.

*/

if (IS_ERR(f) && PTR_ERR(f) == -35)

/* Abre /dev/null. */

f = do_filp_open(dfd, "/dev/null", flags, mode);

//=========================== FLOWX =======================

/* No pudo abrir ni filename ni /dev/null. */

if (IS_ERR(f)) {

put_unused_fd(fd); /* Devuelve el fd requerido. */

fd = PTR_ERR(f);

} else {

/* Notifica que el archivo ha sido abierto. */

fsnotify_open(f->f_path.dentry);

/* Instala el archivo en el descriptor de archivo. */

fd_install(fd, f);

}

}

putname(tmp);

}

return fd;

}

Page 126: Flowx: Implementación de no interferencia en Linux

Capítulo 8

Persistencia de las clases de acceso de losdispositivos

Hasta ahora solo se consideró al dispositivo como un archivo más dentro de nuestro modelo. Suinodo asociado era etiquetado al momento de su creación con la clase de acceso mínima y no se tuvoen cuenta ningún mecanismo para modi�car esa clase. Sin embargo, los dispositivos constituyen elentorno alrededor del cual nuestro módulo ejecutará y, por ende, clasi�carlos con diferentes clases deseguridad es de suma importancia para lograr la con�dencialidad de nuestro sistema. La razón por lacual los dispositivos deben ser tratados en forma diferenciada es porque deben ser con�gurados con lasclases de seguridad que le correspondan en el momento de iniciar el módulo ya que desde la versión2.6 del núcleo, los archivos de dispositivos (es decir el contenido de /dev) se crean desde espacio deusuario durante la inicialización del sistema. Esta característica conlleva a numerosos problemas, yaque los mecanismos que posee el kernel para interactuar con el espacio de usuario deben manejarsecuidadosamente sin destruir la separación entre ambos espacios.

8.1. Especi�cación

A diferencia de los archivos regulares, los dispositivos en Flowx poseen dos clases de acceso: unapara la escritura y otra para la lectura. No siempre las dos clases serán signi�cativas para todos losdispositivos, por ejemplo, de nada sirve la clase de acceso para lectura en una impresora. A pesar deesto, para generalizar y uni�car la implementación, todos se registrarán con ambas clases.

Queremos que estos niveles de acceso puedan ser con�gurados en cada inicio del sistema y que,también, puedan ser cambiados dinámicamente durante la ejecución del mismo. El nivel de salida puedeestar sujeto a quien sea el usuario especí�co en un momento determinado. Como ejemplo, supongamosque un usuario que puede leer información secreta esta usando el sistema, lo que implica que el nivel desalida de la terminal que este utilizando debería tener una clase de acceso igual al nivel de la informaciónque esta accediendo. De lo contrario, no podría ver nada en pantalla. Si este usuario termina su sesióny luego otro, con una credencial menor, requiere la utilización del sistema en la misma terminal, el nivelde salida de la misma debería adaptarse al nuevo usuario, evitando que pueda leer documentos conun nivel mayor al que este posee. Detalles de cómo detectar qué nivel de terminal corresponde a cadausuario serán vistos en el capítulo 9. Lo importante aquí es resaltar la importancia en el dinamismode las clases de acceso en algunos dispositivos.

No obstante, tendríamos que imponer una máxima clase de acceso de entrada como de salidapor defecto. Esto debería resultar de un estudio de la ubicación física del sistema a modo de poderasegurarnos que, más allá de poder cambiar dinámicamente la clase de salida de un dispositivo, elsistema no pueda divulgar información que sobrepase el máximo estipulado (por lo menos a través detal dispositivo). De esta forma, siguiendo el ejemplo anteriormente mencionado, podríamos restringir

124

Page 127: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 8. PERSISTENCIA DE LAS CLASES DE ACCESO DE LOS DISPOSITIVOS 125

qué tipo de usuarios podrían usar una terminal dada, prohibiendo que usuarios muy altos en el rangode seguridad de la empresa puedan, accidentalmente o no, utilizar ciertas terminales potencialmenteinseguras para tal nivel de seguridad.

El máximo también podría ser cambiado pero no dinámicamente como las otras clases y sujeto aun estudio riguroso del por qué del cambio.

Para con�gurar las tres clases mencionadas (entrada, salida y máximo de salida) en el momento quese carga el sistema, debemos disponer de un archivo de con�guración donde se detalle cada dispositivocon sus respectivas clases. La clase máxima de entrada estará simpre sujeta a la máxima de salida. Siun dispositivo no se menciona en este archivo, el mínimo de todas las clases será supuesto (nivel 0).Los detalles de cómo se usa este archivo se desarrollan en la sección 8.2, mientras que el mecanismopara cambiar la clase de salida dinámicamente se detalla en la sección 8.3.

8.2. Mecanismo para establecer las clases de acceso a los dispositivos

Dado que /dev es un directorio temporal, decidimos guardar la relación entre dispositivos y clasesde acceso en el archivo de con�guración /tcb/etc/devices_con�g con el siguiente formato:

[DISPOSITIVO] [SALIDA MAX] [ENTRADA] [SALIDA]

donde el espacio debe ser agregado por el tabulador.El primer obstáculo que debimos enfrentar fue cómo leer el archivo con�guración desde espacio

del kernel para obtener las clases de los dispositivos existentes. La primera solución que analizamosfue: ¾por qué no usar las llamadas tradicionales para manejo de archivos desde dentro del kernel? Másallá de que la idea se pueda concretar (usando varios artilugios), es mal vista por la comunidad dedesarrolladores de Linux [18]. Una de las argumentaciones en contra de una solución de esa naturalezase introduce a continuación:

El kernel no es un proceso. Está diseñado para desarrollar funciones en nombre de unproceso que lo invoque. Como no es un proceso, no tiene contexto y un descriptor de archivosin contexto no vale nada. Por ejemplo, todos los procesos son creados con los descriptoresSTDIN_FILENO, STDOUT_FILENO y STDERR_FILENO (0, 1 y 2, respectivamente),los cuales son asociados a algún dispositivo de entrada/salida. Si estos descriptores noestuvieran asociados a un contexto, todo proceso realizaría operaciones de E/S desde lamisma cosa.

En todo momento, el contexto de un proceso es representado por el objeto current. Este esun puntero a una estructura que contiene todos los elementos necesarios para que el procesopueda resumirse cuando el kernel retorne. También contiene valores que, de manera única,de�nen al proceso y permiten al kernel realizar tareas en su nombre.

Leer o escribir archivos dentro del kernel, requiere un contexto. Uno puede robarlo de algúnproceso existente o puede crear uno. Robarlo implica una gran probabilidad de corromperel proceso del cual fue robado el contexto. Esto sucede porque la operación de E/S serárealizada dentro de su dirección de memoria y se puede llegar a sobreescribir datos enmemoria que el proceso todavía esté usando. Crear un nuevo contexto involucra crear unkernel thread. Este thread, en teoría, funcionaría adecuadamente y podría desarrollar E/Ssobre archivos.

Ahora, la única razón para realizar E/S sobre archivos dentro del kernel es debido a algún�ejercicio intelectual�. No hay ninguna razón para la realizar tareas de modo de usuariodesde el kernel.

La manera en que Unix/Linux está diseñado siempre favorecerá a que las operaciones deE/S se lleven a cabo en espacio de usuario. Es así como se supone que debe ser realizado

Page 128: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 8. PERSISTENCIA DE LAS CLASES DE ACCESO DE LOS DISPOSITIVOS 126

en este tipo de sistema operativo. Si alguien está diseñando algo que requiera E/S sobrearchivos desde dentro del kernel, entonces, el diseño es defectuoso.

El kernel no es una API de �super rendimiento� donde uno puede ejecutar cierto códigocon una ventaja de desempeño. Es un código común que puede ser ejecutado por todos losprocesos dentro del contexto del proceso invocador. Esto lo hace rápido. El núcleo realizalectura y escrituras sobre un archivo a través de código que no puede ser modi�cado portareas de usuario, y por ende, sigue reglas necesarias para mantener sistemas de archivo.El kernel, por lo tanto, es un mecanismo para mantener la sanidad del sistema. El trabajoen sí es hecho por el proceso mismo.

Ya descartada la primera opción, debíamos encontrar otra que satisfaga los requerimientos denuestro modelo y que sea de buen gusto para la comunidad Linux en general. Solucionar el problemaimplica tener en cuenta dos aspectos:

El kernel debe invocar alguna aplicación de usuario para que lea el archivo de con�guración y lepase su contenido. Pero, ¾cuándo y cómo?

El espacio de usuario debe encontrar alguna manera de comunicar al kernel el contenido delarchivo. Agregar nuevas llamadas al sistema tiene que ser el último recurso. Entonces, ¾cómo?

Investigando y estudiando sobre el tema, nos topamos con un proyecto que se encontró con lasmismas di�cultades que nosotros, y que su aceptación y elegancia estructural, nos llevo a imitar sufuncionamiento. Se trata del programa Udev y el sistema de archivos sysfs introducidos en la sección4.8.

Entonces utilizaremo las tecnologías Udev y más sysfs de la siguiente forma:

¾Cómo y cuándo el módulo avisa a espacio de usuario que debe inicializar un dis-positivo?

Cuando el módulo se cargue, debe registrar un nuevo subsistema en el sysfs, llamado �owx. Estenuevo subsistema almacenará kobjetos (directorios) que representarán dispositivos del sistemalos cuales tendrán, cada uno, un atributo (archivo) por defecto para poder leer o escribir la clasede acceso de cada dispositivo.

Cada vez que se cree un nuevo nodo de dispositivo el módulo debe registrar un nuevo directorioen sysfs con el mismo nombre que el nodo.

Por último debe noti�car al programa /tcb/bin/�owx_udev en espacio de usuario, para queinicialice las clases de dispositivos recientemente creados tomándolas del archivo de con�guración.Tanto el archivo como el programa estarán en la TCB (ver sección 9.1 para más detalles sobrela TCB).

¾Cómo se inicializan o gestionan las clases de los dispositivos desde espacio deusuario?

Gracias al sysfs, toda escritura a un archivo asociado a un kobjeto se redireccionará a unafunción que de�niremos para ello. Particularmente, a la hora de programar, debemos lograr quecada escritura o lectura a un atributo del subsistema �owx cambie o consulte, respectivamente,la clase de acceso del dispositivo asociado al kobjeto del atributo. De esta forma, no agregamosninguna llamada al sistema.

El único que puede cambiar clases de acceso es el administrador de seguridad (ver sección 9.1para una de�nición de este usuario). Si quiere hacerlas persistentes el administrador de seguridaddebe agregar una entrada al archivo de con�guración para que /tcb/bin/�owx_udev asigne lasclases de acceso iniciales a través de sysfs cuando detecte que el dispositivo fue creado por el

Page 129: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 8. PERSISTENCIA DE LAS CLASES DE ACCESO DE LOS DISPOSITIVOS 127

núcleo (ver item anterior). Por el contrario, si el administrador de seguridad solo modi�ca lasclases de acceso en el sysfs y no en el archivo de con�guración, las clases cambiadas desaparecerántras un reinicio.

Todo dispositivo que no tenga una entrada en el archivo de con�guración tendrá inicialmente losniveles mínimos para el acceso. Esto es fundamental para la seguridad del sistema. Como se veráen el capítulo 9.

Notar que el archivo de con�guración estará directamente relacionado con el nombre del nodode dispositivo y no con el nombre del dispositivo en sí. Por ejemplo, supongamos que tenemosun dispositivo USB llamado �penusb�, cuyo nodo normalmente se crea con nombre �penusb112�.Si el administrador de seguridad quiere que ese dispositivo tenga clases de acceso 3 y 4 (inputy output), debe agregar una entrada al archivo de con�guración con el nombre �penusb112�.Si por alguna circunstancia se crea un nodo del mismo dispositivo pero con diferente nombre,/tcb/bin/�owx_udev no reconocerá el nodo recientemente creado y por lo tanto sus clases serán,como ya dijimos, las mínimas posibles. Esto no abrirá ninguna brecha de seguridad en nuestrosistema, ya que el nuevo dispositivo será tratado como un canal de bajo nivel y por ende ningúntipo de información �sensible� saldrá por éste.

8.3. FAQ sobre la implementación del mecanismo

Para entender mejor cómo se implementó el mecanismo para establecer las clases de acceso de losdispositivos, desarrollaremos nuestra explicación como una sucesión de preguntas y respuestas.

¾Cómo se registrará un dispositivo dentro del módulo?

La estructura �owx_device de�nida en include/�owx_device.h será el objeto que simbolizará unnuevo nodo de dispositivo,

struct flowx_device {

char *name;

struct dentry *dev_dentry;

struct kobject kobj;

sc input;

sc output;

struct list_head devices;

};

donde dev_dentry es el objeto dentry asociado al nuevo archivo especial, name apunta a dev_den-try->d_iname (nombre del archivo), kobj es el kobjeto para registrar el nuevo dispositivo en elsubsistema �owx, input y output son las clases de acceso y devices es la lista que enlaza a todoslos dispositivos.

Cuando se inicializa esta estructura, a través de �owx_device_init() se toma una referenciaal dentry con dget() para que no sea removido de dcache. Dicha referencia se devuelve en�owx_device_free() con dput().

El campo dev_dentry es útil a la hora de borrar un nodo (sys_unlink()), para así reconocer siel dentry a borrar corresponde al de un dispositivo y poder liberar la memoria de su estructura�owx_device y removerlo del sysfs.

Además se inicializa la estructura �owx_inode_struct asociada al inodo de dev_dentry. En elcampo type se deja constancia de que el inodo pertenece a un archivo especial de dispositivo (type&= I_DEV ) y se almacena en security_data la estructura �owx_device recientemente alojada.

Page 130: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 8. PERSISTENCIA DE LAS CLASES DE ACCESO DE LOS DISPOSITIVOS 128

La cabeza de la lista de todos los dispositivos será la variable struct list_head current_devicesde�nida en �owx_sysfs.c. Junto a ésta se declara un semáforo para controlar el acceso concurrentea la lista.

¾Cuándo se decide crear un nuevo directorio el sysfs y avisar a �owx_udev?

Después de que se creó exitosamente un nodo de dispositivo. Como el único hook que podemosusar en sys_mknod() se invoca antes de, efectivamente, crear el archivo especial, tuvimos queagregar un nuevo hook al LSM. Hay dos razones por las cuales se necesita este hook. La primeraes que para almacenar correctamente el objeto dentry en dev_dentry debemos hacerlo despuésde haberse creado el nodo, ya que este puede cambiar durante la llamada �le->f_op->mknod().La segunda es que debemos noti�car al espacio de usuario una vez que se creo efectivamente elnodo de dispositivo, no antes. El hook se denominó security_inode_post_mknod().

¾Cómo crea el módulo el subsistema �owx?

Para crear nuestro subsistema primero se desarrollaron los siguientes pasos, en �owx_sysfs.c:

1. Se de�ne la estructura �owx_attribute, el cual es simplemente un envoltorio de un atributobásico junto con funciones show y store para redireccionar lectura y escritura del atributocuando esto suceda.

struct flowx_attribute {

struct attibute attr;

ssize_t (*show) (struct flowx_device *dev, char *page);

ssize_t (*store) (struct flowx_device *dev, size_t count, char *page);

};

2. Se crea una instancia de �owx_attribute denominado levels. Este será el default para todoslos kobjetos que se registren en nuestro subsistema. La funciones show() y store() serán:

size_t �owx_attr_read(struct �owx_device *dev, char* page)Devuelve una cadena de caracteres de la forma �in:out� donde in es dev->input->levely out es dev->output->level.

size_t �owx_attr_write(struct �owx_device *dev, size_t count, char* page)Toma una cadena de caracteres de la forma �in:out� donde in se almacenará en dev->input->level y out en dev->output->level.

3. Se de�ne una función release() para los kobjetos de nuestro subsistema, �owx_object_-release(), que remueve el �owx_device, asociado al kobjeto, de la lista de todos los disposi-tivos y libera memoria.

4. Se declara una variable de tipo sysfs_ops, �owx_sysfs_ops, inicializada con la funciones�owx_sysfs_show() y �owx_sysfs_store(). Recordemos que estas serán parte del tipo, ktype,de nuestros kobjetos y, por ende, serán las primeras funciones invocadas al tratar de accedera los atributos de estos. Lo que debe hacer cada función es delegar la tarea a la llamadacorrespondiente al atributo que se quiera acceder. Por ejemplo:

static ssize_t flowx_sysfs_show (struct kobject *kobj,

struct attribute *attr, char *page)

{

struct flowx_attribute *fx_attr = to_flowx_attr(attr);

struct flowx_device *dev = to_flowx_dev(kobj);

ssize_t ret = 0;

Page 131: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 8. PERSISTENCIA DE LAS CLASES DE ACCESO DE LOS DISPOSITIVOS 129

if (fx_attr->show)

ret = fx_attr->show(dev, page);

return ret;

}

5. Se declara el arreglo de atributos por defecto, �owx_default_attrs, con el único atributolevels creado en el segundo item y se usa junto a �owx_sysfs_ops y �owx_object_release(),para de�nir el tipo ktype_�owx.

6. Se declara nuestro nuevo subsistema con tipo ktype_�owx a través de la macro decl_subsys.

¾Cómo se registra el subsistema �owx en sysfs?

La funciones �owx_sysfs_init() y �owx_sysfs_remove() registran y quitan, respectivamente, elsubsistema �owx del sysfs, a través de las rutinas sysfs_subsys_register() y sysfs_subsys_unregis-ter().

¾Cómo �owx_inode_post_mknod() añade el nuevo device?

�owx_inode_post_mknod() es la implementación del hook inode_post_mknod() mencionado enla segunda pregunta. Esta función añade el nuevo dispositivo a través de la función �owx_subsys_-add(), la cual toma el objeto dentry como argumento y:

1. Toma el semáforo dev_sem.

2. Crea un nuevo �owx_device y lo agrega a los current_devices.

3. Inicializa los campos del kobjeto asociado al nuevo dispositivo, para poder ser registradocorrectamente en el subsistema �owx. Estos son kobj->parent, kobj->kset y kobj->ktype.

4. Agrega el kobjeto a nuestro subsistema.

5. Noti�ca a espacio de usuario a través de la función call_usermodehelper().

call_usermodehelper(char *path, char **argv, char **envp, int wait)

Esta función llama al programa con ruta path con los argumentos de línea de comando argvy el entorno envp. Si wait es cero la llamada es asíncrona, caso contrario, la función duermehasta que el programa retorne. El programa invocada será /tcb/bin/�owx_udev mencionadoen la sección anterior.

¾Cómo se quita un device?

Para esto se utilizará el hook inode_unlink invocado cada vez que se quiere borrar un archivo. Ennuestra implementación del hook, �owx_inode_unlink(), se llama a la rutina �owx_subsys_re-move(). �owx_subsys_remove() toma un dentry, busca si corresponde a un �owx_device y, encaso a�rmativo, remueve del sysfs su kobjeto asociado y le resta uno al conteo de referencia deéste. Es muy probable, que al realizar esto último el conteo llegue a cero, lo que produce unainvocación a �owx_object_release() y que, allí, la �owx_device sea liberada.

¾Cómo se consultará ahora la clase de acceso de un inodo?

Sabiendo que al momento de realizar llamadas al sistemas como read() u open() el sistema trataal inodo de un archivo regular igual que al de un archivo especial, debemos tener especial cuidadoa la hora de adquirir la clase de acceso de estos inodos, distinguiendo de alguna manera a quétipo de archivo pertenecen. El motivo radica en que un inodo regular solo tendrá una clase deacceso posible, tanto para escritura o lectura, por lo que alojar una estructura sc para ambosno es una mala idea. En cambio, el inodo de dispositivo puede contener dos clases, las cualesdecidimos ubicar dentro de la estructura �owx_device que le corresponda.

Page 132: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 8. PERSISTENCIA DE LAS CLASES DE ACCESO DE LOS DISPOSITIVOS 130

La única función que accede al campo inode->i_security de un inodo para consultar su clase deacceso es get_�le_sc() en hooks.c. Aquí será el único lugar donde debemos implementar nuestralógica para averiguar el tipo de inodo sobre el que estamos actuando. El campo type de laestructura �owx_inode_struct es vital para esta tarea, y si su valor contiene la bandera I_DEVpodemos estar seguro de que el inodo pertenece a un archivo de dispositivo y que podemos, demanera segura, transformar security_data en una estructura �owx_device.

Puede ocurrir que el módulo haya sido cargado en una etapa posterior a la inicialización del kernel,por lo que puede haber dispositivos que no hayan sido inicializados por nuestras funciones y nohayan sido registrados en nuestro subsistema del sysfs. Pero esto no constituye un problema,ya que la estructura �owx_device nunca será alojada para ellos y, por ende, get_�le_sc() noreconocerá a los inodos asociados como pertenecientes a archivos especiales. Entonces tanto paraéstos, como para inodos de archivos regulares, la función actúa normalmente, inicializando laclase con el valor del atributo extendido correspondiente a ésta, en caso de que haya alguno o,con la menor clase, en caso contrario.

Lo ideal es que módulo sea cargado en tiempo de booteo y debería ser obligatorio más adelante.Sin embargo, debemos programar un módulo para todo uso y no podemos dejar de considerarel caso opuesto. Al ser cargado durante está etapa, estará en funcionamiento antes de que /devsea montado, por lo que una vez que esto suceda y los nodos de dispositivos vayan siendo crea-dos, el módulo los detectará y su estructura �owx_device será creada y registrada en el sysfs.get_�le_sc() reconocerá a sus inodos asociados y obteniendo su estructura �owx_device jun-to con el modo de acceso al archivo, devolverá una de las dos clases de seguridad asociadas aldispositivo.

#define IS_DEV (isec->type & I_DEV)

sc *get_file_sc (struct dentry *dentry, int mode)

{

struct flowx_inode_struct *isec = dentry->d_inode->i_security;

.

.

.

if (is_device(dentry->d_inode) && IS_DEV(isec)) {

struct flowx_device *dev;

dev = (struct flowx_device *) isec->security_data;

if (mode == MAY_READ)

return &dev->input;

else

return &dev->output;

}

.

.

.

}

¾Se puede renombrar un nodo de dispositivo ahora?

No. Más allá de que es posible soportar tal operación, no se hizo, ya que también se requiere quehaya un hook pos renombramiento (el cual no existe), y no se quiere abusar tanto del framework

Page 133: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 8. PERSISTENCIA DE LAS CLASES DE ACCESO DE LOS DISPOSITIVOS 131

LSM, añadiendo hooks caprichosamente. La decisión no le quita usabilidad al sistema, dado quees muy improbable que un usuario cambie el nombre del archivo de dispositivo una vez creado.

Page 134: Flowx: Implementación de no interferencia en Linux

Capítulo 9

Usuarios y Autenticación

Es hora de proveer la noción de usuarios en nuestro modelo de seguridad. Los usuarios que ingresenal sistema poseerán, así como los archivos, procesos o dispositivos, un nivel de seguridad determinado.La información que un usuario pueda ver una vez que ingrese al sistema dependerá de su clase deacceso, de la clase de acceso de la terminal y de la clase de acceso de la información. Por otro lado,la forma en que los usuarios inicien sus sesiones es fundamental para la seguridad del sistema pues esnecesario que este quede en un estado seguro a partir del cual el módulo Flowx pueda imponer solosecuencias de estado seguros.

9.1. Especi�cación y diseño del mecanismo de autenticación

Resuelto el aspecto de cómo etiquetar dispositivos, especialmente, terminales, adentrarnos en estenuevo requerimiento no es una tarea muy complicada. Deberemos disponer de algún tipo de mecanismopara validar los diferentes usuarios en nuestro módulo y asignar, a cada uno, una terminal con la clasede acceso que le corresponda.

Podemos, entonces, dividir lo requerido en dos partes:

Desarrollar el sistema de autenticación dentro del módulo y la asignación particular de la tty.Aquí no todo es tan trivial, ya que debemos desarrollar un mecanismo de validación seguro ytransparente. Para esto, tendremos que estudiar la forma tradicional en la que Linux desarrollaesta tarea y modi�carla para adaptarla a nuestro caso. Además es esencial disponer de algunabase de datos simple, que especi�que qué usuarios pueden ingresar al sistema y con qué nivel determinal.

Determinar el encargado de con�gurar la base de datos mencionada. Inmediatamente, debemosinstalar en el kernel el concepto de administrador de seguridad o secadm (security administrator)1.Este administrador es el único con privilegios para poder cambiar los niveles de seguridad de losrecursos del sistema (archivos, dispositivos, usuarios, etc.) y su jerarquía estará por sobre la delsuperusuario root.

Tanto los programas como archivos utilizados para este mecanismo deben ser resguardados paraque nadie pueda modi�carlos ni explotarlos maliciosamente. Por lo tanto, todos tendrán que pertenecera la TCB (ver 2.3) de nuestro sistema. Como ya mencionamos anteriormente, este componente proveeservicios básicos para poder imponer las políticas de seguridad del sistema y todos sus integrantesdeben ser analizados para certi�car que solo desarrollan las tareas para las cuales están programadosy nada más. Esto evita la posibilidad de que software malicioso se encuentre dentro de la TCB, lo queprovocaría estragos en la seguridad del sistema. Solo el administrador de seguridad puede modi�carla.

1Más allá que mencionamos el secadm en otras secciones, siempre fue visto como una idea y recién ahora se convertiráen realidad.

132

Page 135: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 133

Como conclusión, si el mecanismo de autenticación proviene completamente de servicios brindadospor la TCB, y ésta solo puede ser alterada por secadm, nuestro problema se resume en:

Lograr que un usuario se valide al sistema solo usando servicios de la TCB.

Lograr que nadie pueda hacerse pasar por el administrador de seguridad, lo que implica circu-larmente, que el primer item este correctamente implementado. Además el kernel debe sabertodo el tiempo quién es este usuario, otorgarle privilegios especiales y prohibir que el resto de losusuarios también los posean.

A su vez hay que evitar que el administrador de seguridad ejecute código fuera de la TCB puesde lo contrario podría ejecutar un caballo de Troya que utilice los privilegios de aquel paracomprometer el sistema.

Teniendo en cuenta la forma en que Linux autentica a los usuarios (ver sección 4.9.3) y los requisitosde seguridad para preservar la no interferencia mencionados más arriba, el mecanismo de autenticaciónde Flowx funciona de la siguiente forma:

1. El usuario secadm tiene UID 1.

2. Se implementa un camino con�able (ver sección 2.2.2) que los usuarios deben invocar para unlogin seguro (ver implementación en la sección 9.3).

3. El usuario invoca el camino con�able.

4. El núcleo recibe dicha invocación y ejecuta el programa �ogin a nombre de secadm.

5. �ogin solicita usuario y contraseña.

6. Las contraseñas de los usuarios se almacenan en /tcb/etc/users/ en un archivo por usuario.

7. Valida al usuario y obtiene su clase de acceso.

8. Cambia la clase de acceso de la terminal al mínimo entre la clase de acceso del usuario y la clasede acceso de salida máxima de la terminal (este dato lo obtiene del archivo de con�guración dedispositivos, ver sección 8.2).

9. Invoca al programa login con la opción -f que hace que el programa no solicite ni usuario nicontraseña (usuario preautenticado). login se invoca en nombre de root.

10. login realiza la contabilidad de usuarios (ver sección 4.9.3.3).

11. Finalmente lanza el intérprete de comandos indicado para el usuario en /etc/passwd con el UIDdel usuario.

Ahora estamos en condiciones de explicar el diseño para el requerimiento que motivó esta sección.Lo primero que tendremos que hacer es representar la jerarquía de la Figura 9.1 en el kernel.

Esta jerarquía solo vale para realizar llamadas de la familia setuid y no para otros tipos de privilegiosen el sistema. Para que el usuario pueda ingresar al sistema y podamos asegurar que el mecanismo através del cual lo hace es seguro, deberemos implementar un camino con�able (trusted path, ver próximasección) que al momento de ejecutarlo, lanzará nuestro programa login con las credenciales del secadm.Por lo tanto, debemos lograr que un secadm (a partir de ahora UID 1) pueda realizar llamadas setuidpara convertirse en root o en cualquier usuario normal. Al mismo tiempo, no debemos permitir que niroot, ni ningún otro tipo de usuario pueda transformarse en el administrador de seguridad. Además,ningún programa que pertenezca a secadm tendrá el bit setuid activado, por ende, en ningún momento,nadie podrá tener este tipo de privilegios.

Page 136: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 134

Figura 9.1: Jerarquía de usuarios con respecto a llamadas setuid.

De esta forma, nuestro programa login, el cual llamaremos �ogin (�owx login), ejecutará todo eltiempo con UID 1. Gracias a esto podrá con�gurar el nivel de la terminal especí�co del usuario quehaya sido validado y después cambiar el UID a root. Pero, ¾por qué a root? La respuesta es un tantocompleja.

Como mencionamos anteriormente, la TCB debe estar constituida sólo por software validado yconsiderado seguro. La mayoría de las rutinas que login invoca son funciones de la librería estándar C,las cuales incluyen envoltorios de llamadas al sistema o servicios básicos, como llevar la contabilidad(accounting) del ingreso/egreso de usuarios en el sistema. Lamentablemente, la TCB no puede con�aren la librería C pues esto implicaría veri�carla por completo; todo el código de la TCB debe ser C puroy, si quisiéramos usar alguna biblioteca, tendría que ser una programada por nosotros. Esto conlleva aque las aplicaciones de la TCB tengan que ser simples, de no muchas líneas de código y con su objetivoclaro y de�nido.

Particularmente, �ogin lo único que necesita hacer es validar al usuario usando una base de datosinterna y con�gurar el nivel de la tty adecuadamente. El resto del trabajo lo pueden realizar aplicacionesfuera de nuestro sistema con�able, las cuales incluyen las tareas que realiza login, pues no son críticaspara preservar la no interferencia.

Es por esta causa que una vez que �ogin desempeñe su trabajo, cambiará al usuario a root einvocará a login. Gracias a que este último posee una opción de ejecución para usuarios ya validados(opción -f), no se imprimirá nuevamente el prompt, ni se requerirá la contraseña; lo único que haráserá la administración estándar y llamará al intérprete de comandos con el UID particular del usuario.

La Figura 9.2 resume el mecanismo de validación de nuestro sistema.

Page 137: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 135

Figura 9.2: Mecanismos de autenti�cación. Linux vs Flowx

Observar qué sucede si con�áramos en cualquiera de los tres programas del mecanismo de Linux.Lo crítico de todo este mecanismo es con�gurar la clase de acceso de la terminal acorde al usuario queingrese.

Si login fuera un caballo de Troya, como es el encargado de con�gurar la terminal una vezautenticado el usuario, podría asignar niveles altos a la terminal para que un usuario malignopueda ver libremente documentos secretos.

Si getty fuera un caballo de Troya, como es el encargado de invocar a login, luego de recibir elusuario, podría ejecutar otro programa malicioso para que termine con el resto de la autenticación.

Si init fuera un caballo de Troya podría invocar a cualquier tipo de programa para iniciar elproceso de autenticación.

Por último, si el usuario no invocara el camino con�able nunca se tendrá la seguridad plena de queel programa que recibe los datos del usuario es realmente seguro y parte de Flowx. El usuario podríaejecutar �ogin directamente, pero esto no daría ninguna garantía de que este programa sea interceptadoy reemplazado por otro para producir un engaño en el proceso de autenticación, por esta razón es quese necesita un camino con�able.

9.2. Implementación de �ogin y setuid()

9.2.1. �ogin

�ogin constituye el centro para la correcta autenticación del usuario. Es ejecutado producto de unainvocación al camino con�able del módulo y sus tareas básicas consisten en:

Page 138: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 136

Imprimir el prompt y requerir el nombre del usuario.

Analizar el directorio /tcb/etc/users.

• Si existe algún archivo con el nombre del usuario, solicita una contraseña. Cada archivo enel directorio consistirá de una línea con el formato

nombre:contraseña:uid:nivel_salida_de_la_tty

En un principio la contraseña no estará encriptada ya que solo el administrador de se-guridad podrá ver el contenido del directorio /tcb/etc/users. En el futuro, sí serán codi�-cadas, pero otros estudios hacen falta al respecto. Si la contraseña ingresada correspondecorrectamente a la del nombre de usuario, asigna a la terminal el nivel especi�cado pornivel_salida_de_la_tty, cambia el UID a root y llama a login con la opción -f.

• Si no existe, llama a login sin la opción de usuario preautenticado y con el nombre ingresadocomo argumento. Además setea el nivel de la terminal a 0. Previo a esto, también, cambialos privilegios a root.

Notar que el programa que realmente cambia el UID del intérprete para que ejecute con losprivilegios de un usuario especí�co es login. Para lograr un comportamiento consistente, el UID de/tcb/etc/users para un usuario especí�co, debe ser el mismo que aquel almacenado en /etc/passwdpara el mismo usuario. Si esto no se veri�ca, no implica una brecha en la seguridad del sistema, debidoa la jerarquía de usuarios que setuid() poseerá a partir de ahora (ver Figura 9.1).

Si el usuario ingresante es secadm no se ejecuta el intérprete de comandos convencional como en losotros casos. En vez de esto, se invoca a un intérprete sencillo pero poderoso en materia de seguridad.Este programa ofrecerá opciones para realizar tareas que sólo el administrador de seguridad puederealizar, las cuales son:

Permitir cambiar la clase de seguridad a archivos.

Permitir cambiar las clases de acceso (entrada y salida) de los dispositivos.

Poder modi�car los archivos de con�guración del módulo, o sea, en /tcb/devices_con�g y aquellosen /tcb/users.

El intérprete tendrá que analizar la entrada y ejecutar alguno de los programas (también de la TCB,especí�camente en /tcb/bin) que provean cada una de estas funcionalidades. Al �nalizar su labor, eladministrador debe salir de su intérprete particular y un nuevo camino con�able debe llevarse a cabopara que otro usuario ingrese al sistema en la misma tty.

9.2.2. Setuid

Llegó el momento de explicar cómo se implementó en el kernel la jerarquía de la Figura 9.1. Paramodi�car el comportamiento de las llamadas sys_setuid() y sys_setgid() usamos los hooks:

int task_setuid (uid_t id0, uid_t id1, uid_t id2, int flags)

int task_setgid (uid_t id0, uid_t id1, uid_t id2, int flags)

Estos son invocados por la familia de llamadas set*uid() y set*gid() respectivamente, por lo que elargumento �ags existe para saber diferenciar el ámbito de la llamada y como interpretar los argumentosid* :

LSM_SETID_ID (1) → Funciones setuid() y setgid() con id0 == uid o gid.

LSM_SETID_RE (2) → Funciones setresuid() y setregid() con id0 == real, id1 == efectivo.

Page 139: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 137

LSM_SETID_RES (4) → Funciones setresuid() y setresgid() con id0 == real, id1 == efectivoy uid2 == salvado.

LSM_SETID_RES (8) → Funciones setfsuid() y setfsgid() con id0 == fsuid o fsgid.

Particularmente, nosotros mediaremos solo en el primer caso.Podemos descomponer la tarea de las respectivas implementaciones, �owx_task_setuid() y �owx_-

task_setgid() en dos sub objetivos. El primero consiste en lograr que ningún usuario (incluido root)pueda requerir un cambio de UID al secadm (UID 1). Esto es muy sencillo y el siguiente código loimplementa perfectamente:

/*

* Si el uid a cambiar no es 1 y el proceso actual no pertenece a secadm,

* negar la operación.

*/

if (id0 == 1 && current->uid != 1)

return -EPERM;

El segundo objetivo es conseguir que el administrador de seguridad tenga las capacidades CAP_-SETUID y CAP_SETGID para poder convertirse en root (las capacidades fueron introducidad enla sección 4.9.2). Para explicar nuestra implementación primero tenemos que ver la implementaciónestándar de setuid.Después de invocar al hook de seguridad especí�co, las llamadas para cambiar lascredenciales del proceso invocan a la función capable() para comprobar si el proceso actual tiene lascapacidades necesarias para desarrollar tal tarea. Cada proceso tiene tres conjuntos de capacidades:

Efectivas: usadas por el kernel para realizar los chequeos de permiso para el proceso.

Permitidas: aquellas que el proceso puede llegar a asumir. Si un thread suelta una capacidad desu conjunto de permitidas, nunca podrá volver a adquirir tal capacidad.

Hereditarias: aquellas que se preservan después de execve().

A través de sys_capset() el proceso puede manipular estos conjuntos, siempre y cuando, posea la capaci-dad CAP_SETPCAP. Los campos de la task_struct, cap_e�ective, cap_permitted y cap_inheritablecontienen dichas categorías.

Si el proceso posee permisos, el cambio se lleva a cabo y se llama al hook security_task_post_-setuid(), encargado de actualizar las capacidades del proceso para adaptarlas a su nueva persona.Por defecto, la función dummy_task_post_setuid() (implementación por defecto del hook en cuestión)realiza lo siguiente:

*effective = *inheritable = *permitted = 0;

if (!issecure(SECURE_NOROOT)) {

if (target->euid == 0) {

*permitted |= (~0 & ~CAP_FS_MASK);

*effective |= (~0 & ~CAP_TO_MASK(CAP_SETPCAP) & ~CAP_FS_MASK);

}

if (target->fsuid == 0) {

*permitted |= CAP_FS_MASK;

*effective |= CAP_FS_MASK;

}

}

return 0;

Page 140: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 138

Concretamente:

Con�gura los tres conjuntos de capacidades del proceso actual en 0 (no tiene permisos pararealizar nada).

Chequea si en las opciones de seguridad del sistema (include/linux/securebits.h) se acepta que elUID 0 tenga todas las capacidades.

Si el UID efectivo es 0, asigna todas las capacidades al proceso.

Caso contrario, el proceso queda sin privilegios en el sistema.

Recordar que mientras nosotros no explicitemos una implementación para un hook particular,siempre se ejecutará el dummy asociado. Por lo tanto, no de�nir nuestra �owx_task_post_setuid()provocará que el sistema se siga comportando de la manera explicada. Pero esto es justamente lo querealmente queremos. Si secadm invoca a setuid() tiene tres caminos válidos:

Querer cambiar a root (0). Si lo logra, dummy_task_post_setuid() privilegiará totalmente alproceso.

Querer cambiar a un usuario no root y no secadm. En este caso, el proceso quedará sin capaci-dades.

Querer cambiar, redundantemente, a secadm. El proceso queda como en el segundo item.

Como el administrador de seguridad no tiene por qué tener privilegios especiales, el comportamientonormal del sistema es válido para nuestro diseño. Lo único que debemos imponer es que en el hooktask_setuid() se agreguen las capacidades CAP_SETUID y CAP_SETGID al conjunto efectivo decapacidades del proceso con UID 1. Obviamente, al término de la llamada, estas nuevas capacidadesserán �borradas� (a menos que se cambie a root), pero el trabajo ya estará hecho y no debemospreocuparnos al respecto.

El código para �owx_task_setuid() queda como sigue:

/*

* Si el proceso actual es secadm, permitir que

* pueda cambiar sus credenciales al finalizar

* este hook.

*/

if (current->uid == 1) {

current->cap_effective |= CAP_SETUID;

current->cap_effective |= CAP_FSETID;

}

9.3. Camino Con�able en Flowx

La última etapa del prototipo consiste en implementar el camino con�able (ver sección 2.2.2)necesario para que un usuario del sistema pueda ingresar de manera segura a éste. Todos los cuidadosy precauciones que tomamos en la sección anterior construyendo nuestro �ogin serán en vano si nologramos proveer una forma para que tal programa no pueda ser sustituido o reemplazado por otro.

A este requerimiento, además, se le suma el concepto de �ventana con�able�, que será un mecanismoa través del cual, el kernel y los programas de la TCB, se podrán comunicar con el usuario �nal. Laidea detrás de esta ventana es programarla para que únicamente estos componentes del sistema puedanusarla y, así, poder comunicar mensajes críticos de forma segura.

Page 141: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 139

Si el lector no está familiarizado con SAK o el manejo de la consola le sugerimos repasar las secciones4.10 y 4.6.6, respectivamente.

Linux ya posee una implementación de camino con�able, la cual se accede a través de una SAK(Secure Attention Key). Esto consiste en presionar un grupo especí�co de teclas que el sistema inter-preta como la solicitud de ejecución de un servicio especial. El problema de la implementación SAKde Linux es que mata todos los procesos en la terminal donde se invocó, lo cual conlleva efectos muydesagradables para el usuario cuando se está trabajando con un servidor de ventanas X. Sin embargo,para nuestro prototipo, el enfoque de Linux, se ajusta de buena manera, ya que no se trabajará consistema de ventanas. El SAK de Linux simplemente mata a todos los procesos de la terminal (exceptoinit) y el encargado de lanzar nuevamente el programa login es init, como se vio en la sección anterior.En nuestro módulo, no podemos dejar que esto pase, ya que el único camino seguro para validar alusuario es el que va a través de �ogin. Por lo tanto, deberemos contemplar en nuestra implementaciónque getty nunca sea reiniciado, sino que lo sea �ogin.

La forma más directa para realizar lo anterior es crear nuestra propia attention key y tratar deimitar el comportamiento de la �gura 9.2 en la sección previa.

9.3.1. Implementación del camino con�able

En nuestro módulo, usaremos la tecla `a' para proveer el camino con�able buscado. A continuaciónse procede a explicar cómo fue implementado y los detalles de especial importancia para su correctofuncionamiento.

Un problema nada menor con la función que servirá de handler para nuestra sysrq, es que ésta serállamada en contexto de interrupción (ver sección 4.3). En este contexto no podemos llamar a ningunafunción bloqueante, o sea, no se pueden requerir ni semáforos, ni locks, ni dormir esperando algúnevento. Esto nos prohíbe usar cualquier tipo de función para poder correr nuestro �ogin desde aquí,ya que la llamada sys_exec() es bloqueante. Por lo tanto, tenemos que recurrir a un medio para poderregistrar lo que debemos hacer y hacerlo en un contexto más adecuado. Aquí es donde nos encontramoscon el concepto de cola de trabajo o workqueue del kernel, introducido en la sección 4.11

En nuestra implementación no usaremos la cola de trabajo prede�nida ofrecida por el kernel ya queno queremos sobrecargar la cantidad de trabajos que ésta posee. En cambio, crearemos una propia.Además se de�ne la estructura request :

struct request {

struct work_struct work;

struct tty_struct *tty;

int done;

} request;

la cual será un envoltorio del trabajo a realizar. La razón para construir este envoltorio es porque eltrabajo diferido necesita saber de alguna forma cuál es la tty en la cual debe actuar. Como la funcióninvocada para cada trabajo debe tener el siguiente prototipo:

typedef void (*work_func_t)(struct work_struct *work);

Como debemos respetar este prototipo, la única manera de pasarle otro argumento es a través delenvoltorio usando la macro container_of() explicada en secciones anteriores.

Ahora estamos en condiciones de lanzar nuestro camino con�able usando diferentes rutinas quepuedan bloquearse sin problemas. Concretamente, el camino con�able fue implementado de la siguientemanera:

La sysrq_key_op �owx_key fue instalada en la tabla sysrq bajo la letra `a' y su manejador esla función sysrq_�owx_op().

Page 142: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 140

sysrq_�owx_op(), simplemente inicializa los campos de la variable global req de tipo structrequest con los valores especí�cos y encola el trabajo en nuestra cola �owx_wq. La variable reqserá global para todos los pedidos. Su campo done es un forma un poco rústica de serializar estospedidos y, su work_struct (campo work) es inicializada con la función �owx_work_func.

static struct request req = {

.work = __WORK_INITIALIZER(req.work, flowx_work_func),

.tty = NULL,

.done = 1,

};

�owx_work_func() crea un nuevo kernel thread a través de la rutina kernel_thread(). Esteproceso constituye el núcleo del camino con�able de Flowx y sus tareas se pueden resumir en:

• Obtener el descriptor del líder de la sesión que posee actualmente la tty.

• Asignar −1 al campo exit_signal de la task_struct del lider. De esta forma, cuando el pro-ceso muera no noti�cará a su padre de esta situación y, por lo tanto, init nunca recibiráaviso de la muerte de aquellos procesos que debe reiniciar cuando mueran y no hará na-da. De otro modo, al mandar SIGKILL a todos los procesos que referencien a la tty, initautomáticamente reiniciará getty, algo indeseable, ya que lo consideramos potencialmentedañino para nuestra seguridad (al estar fuera de la TCB).

• Invoca a la rutina SAK_kill() la cual desarrolla básicamente la misma función que el SAKde Linux: manda SIGKILL a todos lo procesos que posean un descriptor de archivo del nodode la terminal o la referencien en su task_struct. Además, recordando el input compartido,para cada uno llama a la función process_must _die(), la cual se asegura de que la señalsea mandada al proceso aún cuando esté durmiendo, producto de esperar entrada del jefede la sesión.

• Abre el archivo de dispositivo correspondiente a la tty y copia el descriptor devuelto enla salida, entrada y error estándares del nuevo kernel thread. Notar que debemos realizaresto después de SAK_kill, dado que de lo contrario esta rutina terminaría mandando unSIGKILL a nuestro thread también.

• Con�gura los valores por defecto de la nueva tty abierta. Los valores de termios son inicial-izados para el correcto comportamiento de la disciplina de línea.

void set_tty_defaults (struct tty_struct *tty)

{

struct ktermios *termios;

if (!tty)

return;

mutex_lock(&tty_mutex);

termios = tty->termios;

termios->c_cflag &= CBAUD | CBAUDEX | CSIZE | CSTOPB;

termios->c_cflag |= HUPCL | CREAD;

termios->c_cc[VINTR] = 3; /* Ctrl - 'c' */

termios->c_cc[VQUIT] = 28; /* Ctrl - '\\' */

Page 143: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 141

termios->c_cc[VERASE] = 127;

termios->c_cc[VKILL] = 24; /* Ctrl - 'x' */

termios->c_cc[VEOF] = 4; /* Ctrl - 'd' */

termios->c_cc[VTIME] = 0;

termios->c_cc[VMIN] = 1;

termios->c_cc[VSTART] = 17; /* Ctrl - 'q' */

termios->c_cc[VSTOP] = 19; /* Ctrl - 's' */

termios->c_cc[VSUSP] = 26; /* Ctrl - 'z' */

termios->c_iflag = ICRNL | IXON | IXOFF;

termios->c_oflag = OPOST | ONLCR;

termios->c_lflag = ISIG | ICANON | ECHO | ECHOK | ECHOKE;

mutex_unlock(&tty_mutex);

}

Las banderas c_c�ags corresponden a los modos de control, c_i�ags a la forma de procesarla entrada, c_o�ags a cómo procesar la salida, c_l�ags con�guran los modos locales y c_ccdeterminan los caracteres de control (DELETE, retorno de carro, etc).

• Llama a la llamada al sistema sys_setsid(), la cual registra al proceso actual como el nuevolíder de la sesión. Básicamente, setea en 1 el campo current->signal->leader y en NULL elcurrent->signal->tty.

• Invoca a la función proc_set_tty() para realizar el cambio de �dueño� de la tty:

◦ Libera referencias a los PID de grupo y sesión. De esta forma, estos descriptores deproceso llegan a 0 en su conteo de referencia y son liberados.

◦ Asigna a nuestra nueva sesión y nuevo grupo como el actual para la tty.

◦ Inicia el campo signal->tty de nuestro kernel thread para que posea la terminal.

• Asigna, también, −1 al campo exit_signal del nuevo thread, para que cuando este muera(por ejemplo, por un nuevo camino con�able solicitado) no quede como zombie.

• A través de la función kernel_execve() ejecuta el programa �ogin visto en la sección anterior.

Por último, es importante detallar un aspecto que al no haber contemplado provocó bastante doloresde cabeza durante el desarrollo del camino con�able. En la función SAK_kill(), además de mandar laseñal SIGKILL a cada proceso que referencie la tty (tsk->signal->tty), hay que asignar NULL a estepuntero. Si no hiciéramos esto, cuando el líder de la sesión muera (se entregue la señal) podrían ocurrirdos cosas:

1. Que el proceso muera antes de que se asocie la tty a otra sesión. En tal caso, no habría problemas.

2. Que el proceso muera (a través de do_exit()) después de haber asignado la tty a otro grupo (osea, después de haber invocado a proc_set_tty() en nuestra implementación).

En el segundo caso surge un problema ya que do_exit() invoca a la función disassociate_tty()cuando muere el líder de una sesión. Como supuestamente no puede haber dos lideres, el propósitode esta invocación es desasociar la terminal de la sesión y grupo actual que la este controlando. Enesta función, si current->signal->tty es NULL nada ocurrirá y todo seguirá su rumbo normalmente.Caso contrario, la función chequeará si hay un grupo actual usando la terminal y le mandará una señalSIGHUP a todos sus integrantes. Por defecto esta señal mata al proceso receptor el cual en nuestrocaso será el recientemente ejecutado �ogin.

Page 144: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 142

La principal razón por la que esto ocurre es porque el SAK de Linux está diseñado para matar atodos los procesos de una sesión, sin contemplar el caso en que una nueva sesión, inmediatamente, tomecontrol de la terminal liberada. Esto es totalmente comprensible, pues el proceso de autenti�caciónestándar de Linux crea la nueva sesión una vez que el líder anterior haya muerto. Si recordamos lasección 4.9.3.1 donde discutimos el programa init, éste recién reiniciará un nuevo getty (para la ttysin líder), una vez que reciba la señal SIGCHLD de su hijo. Claramente, éste es un ejemplo del caso 1descrito anteriormente, por lo que nada malo sucederá.

9.3.2. Ventana Con�able

La ventana con�able o trusted window esta destinada para que el kernel o los programas de laTCB puedan imprimir en ella, y al ser los únicos que pueden hacerlo, provea a los mensajes impresosun marco de seguridad del cual el usuario �nal puede con�ar ciegamente; es otra forma de caminocon�able. La idea de la ventana es que sea la última línea de la pantalla de cualquier terminal activa yque su contenido siempre permanezca allí, sin que pueda ser ni borrado ni sobreescrito, incluso cuandose abren nuevas consolas, se cambia entre consolas virtuales, se modi�ca la resolución del driver devideo o las fuentes actuales de pantalla.

La implementación de la ventana con�able se hizo únicamente sobre el controlador VGA, ya que altener que modi�car aspectos de muy bajo nivel, la cantidad de estas modi�caciones será mínima y sinafectar a las capas superiores de software. Descartamos la posibilidad de usar framebu�er (ver sección4.6.6), debido a que éste está muy ligado al hardware en sí y nuestra implementación hubiese quedadomuy dependiente de éste.

Notar que en esta etapa del prototipo, no nos será posible utilizar los hooks del framework LSM.La ventana con�able implica sí o sí una modi�cación en el driver de la consola y, lamentablemente,ningún gancho (por razones obvias) es provisto para esto.

Sugerimos repasar la sección 4.6.6 para comprender algunos aspectos del manejos de la consola.

9.3.2.1. Implementación

Miremos un poco el controlador de la consola a más bajo nivel. La estructura que representa a cadaconsola virtual en el kernel se denomina vc y está de�nida en include/linux/console_struct.h.

struct vc {

struct vc_data *d;

struct work_struct SAK_work;

};

El campo d incluye todos los datos necesarios para poder manejar la consola especí�ca. Su tipo esvc_data y es una estructura que contiene mucha información. Un bosquejo de su contenido es elsiguiente:

struct vc_data {

unsigned short vc_num; /* Console number */

unsigned int vc_cols; /* [#] Console size */

unsigned int vc_rows;

unsigned int vc_size_row; /* Bytes per row */

unsigned int vc_scan_lines; /* # of scan lines */

unsigned long vc_origin; /* [!] Start of real screen */

unsigned long vc_scr_end; /* [!] End of real screen */

unsigned long vc_visible_origin; /* [!] Top of visible window */

unsigned int vc_top, vc_bottom; /* Scrolling region */

Page 145: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 143

const struct consw *vc_sw;

unsigned short *vc_screenbuf; /* In-memory character/

attribute buffer */

unsigned int vc_screenbuf_size;

unsigned char vc_mode; /* KD_TEXT, ... */

/* attributes for all characters on screen */

unsigned char vc_attr; /* Current attributes */

unsigned char vc_def_color; /* Default colors */

unsigned char vc_color; /* Foreground & background */

unsigned char vc_s_color; /* Saved foreground & background */

unsigned char vc_ulcolor; /* Color for underline mode */

unsigned char vc_halfcolor; /* Color for half intensity mode */

/* cursor */

unsigned int vc_cursor_type;

unsigned short vc_complement_mask; /* [#] Xor mask for mouse pointer */

unsigned short vc_s_complement_mask; /* Saved mouse pointer mask */

unsigned int vc_x, vc_y; /* Cursor position */

unsigned int vc_saved_x, vc_saved_y;

unsigned long vc_pos; /* Cursor address */

/* fonts */

unsigned short vc_hi_font_mask; /* [#] Attribute set for upper

* 256 chars of font or 0

* if not supported

*/

struct console_font vc_font; /* Current VC font set */

unsigned short vc_video_erase_char; /* Background erase character */

/* VT terminal data */

unsigned int vc_state; /* Escape sequence parser state */

unsigned int vc_npar,vc_par[NPAR]; /* Parameters of current escape

sequence */

struct tty_struct *vc_tty; /* TTY we are attached to */

/* data for manual vt switching */

.

.

.

/* mode flags */

.

.

.

/* attribute flags */

.

.

.

/* misc */

.

.

.

};

Page 146: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 144

Los comentarios originales del kernel bastan para indicar a qué propósito sirve cada campo de vc_data.Particularmente útiles para nuestro requerimiento son los campos que denotan las dimensiones de cadaconsola virtual, más especí�camente, vc_rows que indica la cantidad de �las que contiene la terminalen un momento dado.

Anteriormente, mencionamos que la ventana con�able estaría ubicada al �nal de la pantalla, ocu-pando la última línea de ésta. Esto se aplicará para todas las terminales y para cualquier dimensiónque puedan llegar a tener. Para lograr reservar tal línea la variable vc_rows se decrementará en 1 almomento de la inicialización de la consola, a través de la rutina con_init() la cuál forma parte del pro-ceso de inicio del kernel y es llamada por la función start_kernel(). Por ende, la primera modi�caciónen drivers/char/vt.c es:

static int __init con_init(void)

{

.

.

.

for (currcons = 0; currcons < MIN_NR_CONSOLES; currcons++) {

vc_cons[currcons].d = vc = alloc_bootmem(sizeof(struct vc_data));

INIT_WORK(&vc_cons[currcons].SAK_work, vc_SAK);

visual_init(vc, currcons, 1);

vc->vc_screenbuf = (unsigned short *)

alloc_bootmem(vc->vc_screenbuf_size);

vc->vc_kmalloced = 0;

//=========================== FLOWX ==============================

//orig vc_init(vc, vc->vc_rows, vc->vc_cols,

/*

* vc_init() es donde se inicializan casi todos los campos

* de la estructura vc_data.

*/

vc_init(vc, vc->vc_rows - 1, vc->vc_cols,

//=========================== FLOWX ==============================

currcons || !vc->vc_sw->con_save_screen);

}

.

.

.

}

Ahora que tenemos nuestro espacio reservado, debemos diseñar una forma de impresión seguraen éste. Se creó una nueva llamada al sistema para poder escribir desde espacio de usuario (solo porprogramas de la TCB):

asmlinkage ssize_t sys_trusted_syslog(const char * buf, size_t count);

Para realizar su tarea, primero consulta a is_trusted() para determinar si el proceso solicitante provienede un programa de la TCB, toma la cadena buf desde el espacio de usuario con strncpy_from_user() y,�nalmente, llama a la rutina trusted_print(), la cual transforma la cadena de caracteres en un formatoválido para ser impreso en pantalla (recordar que cada caracter es de 1 byte de longitud, contra 2 bytesque representan cada celda en la memoria de video), con�gurando el color de cada caracter y su fondo.

Page 147: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 145

Esta nueva cadena formateada es copiada en la variable global console_status_line la cual almacena loque debe estar impreso, en un momento dado, en la ventana con�able. Para concluir, trusted_print()invoca a �ush_console_status_line() para pegar el contenido de la variable global en la porción dememoria de video que corresponda a la última línea de la pantalla.

En caso que se quiera imprimir un mensaje desde el kernel, hay que usar trusted_print(), sin tenerque pasar por la llamada al sistema. Todas las implementaciones de estas funciones se encuentran enkernel/printk.c y están de�nidas en include/linux/kernel.h.

Todo este proceso no alcanza para cumplir el requerimiento, ya que una vez impreso el mensaje re-querido, debemos ser capaces de mantenerlo constante en pantalla. Para esto, debemos estudiar cuándose actualiza el contenido de la pantalla y cómo nuestras modi�caciones afectan a su comportamiento.

Un caso donde la pantalla debe ser redibujada es cuando se realiza un desplazamiento hacia abajoo arriba del contenido actual de la terminal. Las funciones scrdown() y scrup() respectivamente:

Chequean que el fondo actual de la consola (vc_bottom) no sea mayor a su cantidad de líneas(vc_rows).

Determinan el nuevo origen y �n de la pantalla, o sea, qué segmento de la memoria de video debeser mostrado.

Mueven este contenido calculado efectivamente en la memoria.

Al �nal de este movimiento debemos imprimir nuevamente el contenido de la ventana con�able ya queen otro caso será borrado. Para esto se llama a �ush_console_status_line() como última sentencia deestas funciones. Además, en el chequeo del primer item, si no re�ejamos las verdaderas dimensiones dela consola (vc_rows + 1) el scroll no se realizará, por lo que también hay que modi�car esto.

static void scrup(struct vc_data *vc, unsigned int t, unsigned int b, int nr)

{

unsigned short *d, *s;

if (t+nr >= b)

nr = b - t - 1;

//=========================== FLOWX ==============================

//orig if (b > vc->vc_rows || t >= b || nr < 1)

if (b > vc->vc_rows + 1 || t >= b || nr < 1)

//=========================== FLOWX ==============================

return;

if (CON_IS_VISIBLE(vc) && vc->vc_sw->con_scroll(vc, t, b, SM_UP, nr))

return;

d = (unsigned short *)(vc->vc_origin + vc->vc_size_row * t);

s = (unsigned short *)(vc->vc_origin + vc->vc_size_row * (t + nr));

scr_memmovew(d, s, (b - t - nr) * vc->vc_size_row);

scr_memsetw(d + (b - t - nr) * vc->vc_cols, vc->vc_video_erase_char,

vc->vc_size_row * nr);

//=========================== FLOWX ==============================

flush_console_status_line();

//=========================== FLOWX ==============================

}

Otro aspecto que debemos considerar es que no podemos permitir al sistema hacer desplazamientopor hardware, ya que por su naturaleza no nos permitiría reservar la línea que necesitamos. Paraprohibirlo, cambiamos lo siguiente en vgacon_startup() en drivers/video/vgacon.c:

Page 148: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 146

static const char *vgacon_startup(void)

{

.

.

.

//=========================== FLOWX ==============================

//orig vga_hardscroll_enabled = vga_hardscroll_user_enable;

vga_hardscroll_enabled = 0;

//=========================== FLOWX ==============================

.

.

.

}

Cuando se cambia el tamaño de la terminal debemos también mantener nuestra última línea.Cuando se asigne el nuevo tamaño de �las de la nueva dimensión tendremos que restar uno también.Además, la invocación a �ush_console_status_line() nuevamente es necesaria. Concretamente:

int vc_resize(struct vc_data *vc, unsigned int cols, unsigned int lines)

{

.

.

.

new_cols = (cols ? cols : vc->vc_cols);

//=========================== FLOWX ==============================

//orig new_rows = (lines ? lines : vc->vc_rows);

new_rows = (lines ? lines - 1: vc->vc_rows);

//=========================== FLOWX ==============================

.

.

.

//=========================== FLOWX ==============================

flush_console_status_line();

//=========================== FLOWX ==============================

return err;

}

Cada vez que se quiere redibujar la pantalla se llama, en última instancia, a la función redraw_-screen() en drivers/char/vt.c. Esta función determina si su invocación pertenece a un cambio de consolao no, y llama a la rutina especí�ca del driver de video vgacon_switch():

update = vc->vc_sw->con_switch(vc);

con los datos de la consola actual. Esta función también se llama cuando queremos cambiar entre conso-las virtuales. Aquí se realiza una comprobación sobre el tamaño total de la pantalla y, lamentablemente,usa vc_rows para obtener dicha información. En todos estos lugares debemos noti�car que el tamañoreal es vc_rows + 1, ya que de lo contrario nunca se imprimirá nada en la última línea.

static int vgacon_switch(struct vc_data *c)

Page 149: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 9. USUARIOS Y AUTENTICACIÓN 147

{

int x = c->vc_cols * VGA_FONTWIDTH;

//=========================== FLOWX ==============================

//orig int y = c->vc_rows * c->vc_font.height;

int y = (c->vc_rows + 1) * c->vc_font.height;

//=========================== FLOWX ==============================

.

.

.

vga_video_num_columns = c->vc_cols;

//=========================== FLOWX ==============================

//orig vga_video_num_lines = c->vc_rows;

vga_video_num_lines = c->vc_rows + 1;

//=========================== FLOWX ==============================

.

.

.

if (!vga_is_gfx) {

scr_memcpyw((u16 *) c->vc_origin, (u16 *) c->vc_screenbuf,

c->vc_screenbuf_size > vga_vram_size ?

vga_vram_size : c->vc_screenbuf_size);

if ((vgacon_xres != x || vgacon_yres != y) &&

(!(vga_video_num_columns % 2) &&

vga_video_num_columns <= ORIG_VIDEO_COLS &&

vga_video_num_lines <= rows))

//=========================== FLOWX ==============================

//linux vgacon_doresize(c, c->vc_cols, c->vc_rows);

vgacon_doresize(c, c->vc_cols, c->vc_rows + 1);

//=========================== FLOWX ==============================

}

vgacon_scrollback_init(c->vc_size_row);

return 0; /* Redrawing not needed */

}

Para �nalizar, cabe destacar que la implementación de la ventana con�able no permite desplaza-mientos dentro de ésta, o sea, que la longitud de los mensaje que se quieran imprimir en ella no puedesuperar el ancho de pantalla actual.

Page 150: Flowx: Implementación de no interferencia en Linux

Capítulo 10

Comunicación Entre Procesos

La comunicación interprocesos sienta las bases para una correcta implementación de aplicacionesde múltiples hilos de control (multithreaded) o programas que interactúan mutuamente para desarrollarun tarea en común. El kernel provee mecanismos IPC (InterProcess Communication) que facilitan lasincronización y la compartición de información entre diferentes procesos del sistema. Sin embargo, elkernel no restringe que tipo de información puede ser enviada de un proceso a otro, ni ningún tipo decontrol con respecto a la sensibilidad de dicha información. Particularmente, permitir que diferentess-hermanos tengan total disponibilidad para utilizar recursos IPC constituye un brecha enorme en laseguridad del sistema e indudablemente una intervención considerable deberá llevarse a cabo con el �nsubsanar esta falla. Este capítulo describirá los controles de seguridad que el módulo Flowx imponepara cada uno de los IPC descriptos en la sección 4.12.

10.1. Pipes y FIFOs

En esta sección nos enfocaremos en encontrar un diseño para la seguridad de los pipes y las FIFOSy su posterior implementación. Como vimos en las secciones 4.12.1 y 4.12.2 estos dos recursos solodi�eren en la manera en que son creados, mientras que su acceso y mantenimiento son manejados porlas mismas rutinas del kernel. Por consiguiente, casi todo el análisis se centrará particularmente en lospipes, ya que la seguridad de las FIFOS será una consecuencia directa de los primeros. Cuando algúncontrol especí�co sea necesario para algún recurso y no el otro, se mencionará explícitamente.

10.1.1. Diseño

Un pipe podría verse como un archivo regular que no tiene una correspondiente imagen en disco.Teniendo en cuenta los controles que el módulo impone a los archivos regulares, el pipe presenta ladiferencia y di�cultad de que debe utilizarse para comunicar información entre diferentes procesosy, por ende, su clase de seguridad tendría que ser dinámica. Una clase estática nos limitaría a solocompartir información entre procesos del mismo nivel, lo que sería una perdida en el compatibilidaddel módulo con programas ya existentes. Como ejemplo, supongamos una base de datos que recibeinformación de diferentes procesos a través de pipes. Con la restricción mencionada, la base de datossolo podría recibir información de los procesos con su mismo nivel de seguridad, cuando idealmente,debería recibirla de todos los procesos con nivel menor o igual a ella. Esto nos obliga a pensar en undiseño que permita realizar esto último y que asegure que la información jamás �uya de arriba haciaabajo. En una primera instancia se podría requerir que:

Un proceso pueda abrir un pipe como escritura, si y solo si, no existe ningún otro proceso demenor nivel escuchando en el otro extremo.

148

Page 151: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 149

Un proceso pueda abrir un pipe como lectura, si y solo si, no existe ningún otro proceso de mayornivel escribiendo en el otro extremo.

Hasta aquí todo parece sencillo, pero, ¾cómo saber qué procesos posee el otro extremo de un pipe?Esta es una pregunta muy difícil de responder ya que, tanto en sys_read() como en sys_write(),nosotros solo poseemos el descriptor de archivo de un extremo de la tubería, no existiendo ningunaestructura de datos en el kernel que nos informe cuál es el otro objeto �le asociado al pipe.

Suponiendo que conociéramos ambos objetos �le surge otra di�cultad: Si no se cumple alguna delas reglas mencionadas en los items anteriores, ¾qué hacer? Basados en nuestras experiencias anteri-ores sabemos que simplemente negar el acceso repercutiría signi�cativamente en la compatibilidad delmódulo. Por ende, debemos diseñar una forma en que procesos de igual nivel se puedan comunicarlibremente y que, el prohibir la comunicación entre diferentes clases, no afecte la performance del resto.

Una opción sería usar el pipe para todos los niveles y que los procesos se vayan turnando en suuso. Cada vez que un proceso no tenga permisos para acceder al recurso (ej. hay un proceso de mayornivel escribiendo) simplemente es dormido hasta que el camino este despejado para su nivel (Figura10.1). Sin embargo, tener un recurso para todas las clases de acceso afecta en el desempeño. Además,si recordamos que los procesos ya pueden dormir en un pipe (ej. bu�er lleno), se generan problemas deconcurrencia muy grandes.

Figura 10.1: P0 y P1, C0 y C1 s-hermanos. El subíndice representa el nivel.

Como ejemplo supongamos que P0 escribe en el pipe, luego C0 lee todo lo escrito. A continuación,C0 intenta una nueva lectura pero al no haber más información, duerme. Si ahora P1 intentase escribir,este sería dormido porque todavía hay un proceso de menor nivel escuchando del pipe. Pero, ¾dóndedormir y hasta cuándo? ¾Cómo avisará C0 que terminó de leer y se pueda despertar a P1? Además elkernel noti�ca a los suscriptores de una tubería cuando ya es inútil realizar alguna operación debidoa la inexistencia de procesos en el otro extremo. Generalmente, se retorna EOF en caso de lectura o-EPIPE en caso de escritura. Esta noti�cación solo se produce cuando se libera algunos de los objetos�le en alguno de los extremos del pipe. Liberar un �le implica que no puede ser más usado, entonces,cuando C0 deje de escuchar, ¾cómo avisar a P0 que deje de escribir sin perjudicar a P1?

Buscando respuestas para todas estas preguntas nos dimos cuenta de que el enfoque era demasiadocomplicado, tedioso y defectuoso para ser implementado. Inmediatamente surgió el de la Figura 10.2.

Page 152: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 150

Figura 10.2: PIPE0 y PIPE1: pipes de niveles 0 y 1 respectivamente. F : objetos �le.

Aquí creamos un nuevo pipe para cada nivel, pero usamos los mismos objetos �le para accederlos,multiplexando su uso. Para realizar esto, el objeto dentry de cada objeto debe ser reemplazado por elque corresponda dependiendo el nivel del proceso que invocó la operación sobre la tubería. El problemaaquí es que cambiar el dentry del archivo puede ser peligroso. Un caso que lo denota es:

P0 quiere acceder a PIPE0. Se cambia el dentry de F − in para que apunte a PIPE0.

P0 duerme en PIPE0.

P1 quiere acceder a PIPE1. Se cambia el dentry de F − in para que apunte a PIPE1.

Ahora P0 despierta pero al dormir en una función interna del kernel no podemos volver a cambiarel dentry para que apunte a PIPE0. Esto implica que toda operación que realice P0 luego dedormir será sobre el pipe incorrecto.

El diseño de�nitivo se basó en este último pero, en vez de usar el objeto dentry para multiplexar losdiferentes pipes, se utilizó el mismo enfoque de �envoltorios� que se usó en el capítulo 6 de la entradacompartida. Este consiste en envolver las funciones pipe_read() y pipe_write() con funciones nuestras,que desarrollen tareas antes y después de, efectivamente, acceder a las estructuras internas de un pipe.Cada pipe tendrá un �pipe hermano� de distinto nivel y estos estarán enlazados en una lista. Cuandoun proceso de alto nivel acceda a nuestro envoltorio deberá obtener el pipe que corresponde a su nivel(o crearlo en caso de que no exista) e invocar la llamada pipe_read() o pipe_write() del objeto �leasociado a esa tubería. Entonces, cada pipe hermano tendrá su respectivo par de objetos �le, pero todoproceso accederá a las llamadas al sistema a través de los �les del pipe original. El envoltorio es elencargado de multiplexar los objetos �les. La Figura 10.3 resume el proceso.

Page 153: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 151

Figura 10.3: Caminos que procesos de distinto nivel pueden tomar en sys_read() para un pipe.

Vale mencionar algunos cuidados que debemos tomar al momento de crear un nuevo pipe paraun proceso de cierto nivel. En el capítulo 6 buscamos compartir la entrada de bajo nivel entre loss-hermanos con el objetivo de que éstos siguiesen el mismo camino de ejecución bajo condicionesnormales. Además un proceso de alto nivel merecía completamente recibir toda información ingresadaa través de una terminal de menor nivel a él. Al duplicar un pipe surge inmediatamente el mismoproblema. Una tubería es compartida por diferentes familias de procesos. Algunas escriben, otrasescuchan. Si nosotros duplicáramos el recurso IPC y nos limitáramos a que únicamente procesos delmismo nivel que el recurso puedan escribir en él, estaríamos ignorando lo que algunas familias nosquieran comunicar. Por lo tanto, debemos permitir, o mejor dicho, obligar a que los procesos de bajonivel escriban, no solo en las tuberías que iban a escribir en un principio, sino también en todas aquellastuberías �hermanas� de clase de acceso más alta. Sin embargo, esto es cierto solo si el proceso no poseeya un s-hermano con el nivel del pipe a escribir, ya que se espera que el s-hermano realice o hayarealizado la escritura al seguir el mismo camino de ejecución.

Page 154: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 152

10.1.2. Implementación

Esta sección detallará la implementación de la política de seguridad a adoptar para pipes y FIFOsa través del diseño basado en envoltorios de las principales rutinas que acceden a la información deestos recursos.

Cuando un proceso desea leer o escribir información de un pipe invoca, como ya mencionamos, a lasllamadas al sistema read() y write(), respectivamente. Si recordamos los detalles del capítulo 5, ambasllamadas están intervenidas por el hook �owx_�le_permission(), el cual era el encargado de otorgarpermisos para realizar cada una de estas operaciones. En el capítulo 6, vimos que el requerimientode la entrada compartida también hacia uso de este hook con el objetivo de envolver a las rutinas delectura y escritura de más bajo nivel del VFS, y así realizar tareas antes y después de las verdaderasoperaciones llevadas a cabo por el kernel, las cuales utilizan estructuras y variables a las que nosotrosno podemos acceder.

Para implementar la seguridad en pipes el enfoque no es muy diferente al utilizado para compartirla entrada. Cuando en �owx_�le_permission() se detecte que estamos en presencia de este tipo derecurso, la función wrap_pipe_ops() será invocada con el �n de:

Inicializar una estructura especial para registrar el pipe en el módulo.

Crear un nuevo conjunto de operaciones de archivo (�le_operations) para servir de envoltorio dela operaciones por defecto del pipe.

Envolver efectivamente las operaciones.

En las secciones 4.12.1 y 4.12.2 observamos que el conjunto de operaciones por defecto que se lesasignaba a los objetos �le de cada lado de un pipe o FIFO, dependía del modo en que era abiertotal objeto. Para el caso de las tuberías, se de�nían para cada extremo solo las operaciones read owrite, pero nunca ambas. Para las FIFOs podía ocurrir que tenga ambas o solo una de las dos. Decualquier manera, wrap_pipe_ops() debe comprobar el modo de acceso al archivo y envolver solo larutina que corresponda. Al �nalizar �owx_�le_permission() si la operación es de lectura se invocaráa �owx_pipe_read() y, si es de escritura, a �owx_pipe_write().

Antes de dar detalles de qué realizan estas funciones introduzcamos la estructura especial utilizadapara llevar registro de este tipo de recurso en nuestro módulo; la estructura �owx_pipe.

struct flowx_pipe {

struct list_head s_sib_pipes;

struct file *fdr, *fdw;

struct rw_semaphore *rwsem;

struct inode *inode;

};

Donde:

s_sib_pipes es una lista que contiene a los pipes hermanos para procesos de diferentes niveles.

fdr y fdw son punteros a los objetos �le correspondientes a cada extremo del pipe que posee esta es-tructura. Su utilidad se apreciará mejor al explicar los envoltorios de pipe_read() y pipe_write().

rwsem es un semáforo de lectura/escritura para controlar la concurrencia entre pipes hermanos.

inode es un puntero al inodo que posee el pipe.

Esta estructura debe ser consultada tanto por lectores o escritores del pipe y, por ende, debeubicarse en el único punto en común que tienen estos dos, el inodo asociado a cada �le. De esta manera,�owx_pipe es almacenada en el campo security_data de la estructura �owx_inode_struct asociada al

Page 155: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 153

inodo (ver sección 5.4.2). El campo type es inicializado para registrar que el inodo pertenece a un pipea través de la bandera I_PIPE.

Aquí debemos remarcar una sutileza con respecto a las FIFOs. Estas son creadas a través de lallamada mknod() (ver sección 4.12.2) y si recordamos el capítulo 8 sobre dispositivos notaremos que,luego de crear todo archivo especial a través de esta rutina, registrar un dispositivo implicaba, entreotras cosas, crear una estructura �owx_device, alojarla en el campo security_data de la la estructura�owx_inode_struct y dejar constancia en el campo type de que el inodo pertenece a este tipo derecurso del sistema. Un problema que surge es qué hacer con la FIFOS, ya que son tanto pipes comodispositivos. No podemos registrarlas como ambas porque el campo security_data puede referenciara una sola estructura especial, �owx_device o �owx_pipe. Sin embargo, la elección es fácil: dada lanaturaleza de las FIFOs lo más prudente es tratarlas como tuberías. Por lo tanto, debemos prohibirque el hook �owx_inode_post_mknod() registre a una FIFO como dispositivo, como se muestra en elsiguiente fragmento de código:

void flowx_inode_post_mknod (struct inode *dir, struct dentry *dentry,

int mode, dev_t dev)

{

if (S_ISFIFO(dentry->d_inode->i_mode))

return;

.

.

.

}

Habiendo introducido la estructura �owx_pipe podemos proceder a explicar detalladamente lasfunciones envoltorio �owx_pipe_read() y �owx_pipe_write():

�owx_pipe_read (struct �le *�le, char __user *buf, size_t count, lo�_t *pos)

1. Hace que �le->f_op apunte nuevamente a la estructura �le_operations que apuntaba antesde envolver la operación read(). Para administrar estas estructuras (la original y la quecontiene los envoltorios) se crea una nueva, de�nida como sigue:

struct pipe_fops {

const struct file_operations *orig_ops;

struct file_operations *flowx_pipe_ops;

};

pipe_fops se crea al momento de envolver por primera vez las operaciones de un objeto �leperteneciente a un pipe y el campo �owx_pipe_ops almacena una copia idéntica a orig_opssalvo por el conjunto de rutinas a envolver (dependiendo el modo de acceso). El siguientecódigo de wrap_pipe_ops() muestra con más detalle esto último:

int wrap_pipe_ops (struct file *file, int mode)

{

struct pipe_fops *pipe_ops = file->f_security;

.

.

.

if (!pipe_ops) {

pipe_ops = new_pipe_fops(file);

if (!pipe_ops)

return -ENOMEM;

Page 156: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 154

file->f_security = pipe_ops;

}

*pipe_ops->flowx_pipe_ops = *file->f_op;

if (mode == MAY_READ)

pipe_ops->flowx_pipe_ops->read = flowx_pipe_read;

if (mode == MAY_WRITE)

pipe_ops->flowx_pipe_ops->write = flowx_pipe_write;

file->f_op = pipe_ops->flowx_pipe_ops;

return 0;

}

La estructura pipe_ops se almacena en el campo de seguridad f_security del �le, para unmás rápido acceso a las operaciones a la hora de la restauración.

2. Obtiene la estructura �owx_pipe del �le.

3. Toma el semáforo rwsem de la �owx_pipe obtenida y recorre la lista de pipes hermanosen busca de la tubería que corresponde al nivel del proceso actual. Si no hay ningunatubería asociada a él, entonces crea una copia del pipe de nivel inmediatamente menor a ély la registrar en la lista s_sib_pipes. La rutina �owx_create_pipe() crea la nueva tubería,inicializa su nueva �owx_pipe y asigna a los campos fdr y fdw los objetos �le asociados acada extremo. copy_bu�ers() recibe como argumento dos tuberías y duplica el contenidode una en la otra.

4. De la estructura �owx_pipe obtenida en el item anterior, toma su puntero fdr e invoca ala pipe_read() original usando como argumento tal puntero. De esta forma, pipe_read()tomará el inodo que referencia fdr y realizará todas sus operaciones basándose en éste.

5. Para entender el último paso hay que recordar que un pipe puede ser manejado por diferentesfamilias de s-hermanos. Supongamos que en un momento tenemos una situación similar ala que se ilustra la Figura 10.4. El pipe hermano de nivel 1 fue recientemente creado, en

Figura 10.4: Nuevo PIPE1 creado pero no hay lector de su nivel.

el cual escribe P1. Para preservar la compatibilidad con el software existente, debo crearun nuevo s-hermano de C0 para que pueda procesar la información de alto nivel escritapor P1 y que su hermano no puede procesar. Al �nal de �owx_pipe_read() el proceso debechequear si se creó un nuevo pipe para un nivel superior . En caso a�rmativo, si para talpipe no existe un s-hermano lector debe crear uno de forma tal que reejecute la llamadaread() y lea efectivamente de la tubería de mayor nivel.

Page 157: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 155

�owx_pipe_write (struct �le *�le, const char __user *buf, size_t count, lo�_t *pos)

1. Hace que �le->f_op apunte nuevamente a la estructura �le_operations que apuntaba antesde envolver la operación write().

2. Obtiene la estructura �owx_pipe del �le.

3. Toma el semáforo rwsem de la �owx_pipe obtenida y recorre la lista de pipes hermanos enbusca de la tubería que corresponde al nivel del proceso actual. Si no hay ninguna tuberíaasociada a él, entonces crea una copia del pipe de nivel inmediatamente menor a él y laregistrar en la lista s_sib_pipes.

4. De la estructura �owx_pipe obtenida en el item anterior, toma su puntero fdw e invoca ala pipe_write() original usando como argumento tal puntero.

5. Por último, como todos los datos procesados por procesos de bajo nivel pueden ser vistospor procesos de alto nivel, debemos copiar la información escrita en el pipe objetivo en todoslos pipes hermanos de mayor nivel. Esto ocurre siempre y cuando el proceso actual ya noposea un s-hermano del nivel del pipe hermano a copiar la información. Para realizar estaúltima tarea hay dos alternativas:

Si en la tubería a copiar lo que debemos copiar no sobrepasa el espacio disponible,entonces se nos garantiza que el proceso actual no dormirá en ella y dejamos que éstesea el que realice la copia.

Si la información a copiar sobrepasa lo disponible en los bu�ers, entonces creamos unnuevo thread del kernel para realizar la tarea. De esta forma, no quitamos operatibilidadal escritor original del pipe, ya que el nuevo thread es el que debe esperar a que la tuberíaposea espacio necesario para escribir.

Como puede observarse, dada una familia de s-hermanos, todos ellos invocarán a read() o write()con el mismo objeto �le. Luego, los envoltorios respectivos multiplexarán el acceso a las diferentestuberías, invocando a pipe_read() o pipe_write() con diferentes objetos �le como argumento. No nosdebemos preocupar porque se liberen antes de tiempo los �les originales, ya que durante la llamadafork() cada proceso tomará una referencia a éstos al incrementar sus conteo de referencia. De estaforma, solo se destruirán cuando todos los procesos hayan dejado de usarlos.

Ahora, para liberar los pipes hermanos creados durante �owx_pipe_read() y �owx_pipe_write()usamos la información de que solo los objetos �le pertenecientes a un pipe poseen alojada una estructuraen su campo f_security. Cuando todos los procesos hayan dejado de usar los �les originales y sellame al hook �le_free_security() para liberar la memoria del campo f_security ejecutamos la funciónfree_pipe_fops(). Esta rutina libera la memoria de la estructura pipe_fops alojada previamente enwrap_pipe_ops() y luego, recorre la lista s_sib_pipes para liberar la referencia del objeto �le de cadapipe hermano que posee la tubería original. Dicha referencia se encuentra en el campo fdr o fdw,dependiendo de que extremo del pipe original se este liberando.

Para �nalizar, el hook �owx_inode_free_security() utiliza la información del campo type en laestructura �owx_inode_struct para determinar si un inodo a destruir pertenecía a un pipe y, en casode que así sea, destruye la estructura �owx_pipe asociada y alojada en el campo security_data.

10.2. System V IPC

Una vez impuestos los controles de seguridad para los pipes es hora de hacer los mismo para losdemás mecanismos de comunicación entre procesos. Esta sección buscará, mediante los conocimientosadquiridos en la sección 4.12.3, controlar la interacción de procesos con este tipo de recursos IPC paraasegurar la con�denciabilidad del sistema.

Page 158: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 156

10.2.1. Diseño General

Como ya sabemos, los tres mecanismos que componen a los IPC System V son las colas de mensajes,los semáforos y las regiones de memoria compartida. El requerimiento para cada uno de estos es similarque el de los pipes y lo que se busca es que los recursos IPC siempre sean usados de abajo hacia arribaen materia de escrituras y que, cada proceso lea de su respectivo recurso y de ningún otro más.

Para esto será necesario crear una copia de cada recurso cuando haya más de un s-hermano inter-actuando con él. Cada s-hermano leerá únicamente del recurso asignado para su nivel, pero debemosobligar a los procesos de bajo nivel a siempre actualizar los recursos de los procesos más privilegiados.

Hasta ahora todo parece casi idéntico al diseño especi�cado para las tuberías y siguiendo la mismalínea tendríamos que pensar en los envoltorios para acceder antes que el kernel a los recursos IPC. Sinembargo, los IPC System V no están basados en archivos como si lo están los pipes y la manera para�multiplexar� el uso de los IPC es mucho más sencilla.

Como vimos en la sección 4.12.3, cada recurso tiene asociado un identi�cador IPC muy similar aun descriptor de un archivo regular. Observando el código del kernel, se puede notar que toda llamadaal sistema que trata con los IPC System V, sin importar de qué tipo sea, recibe un identi�cador y através de la función ipc_lock() deriva la estructura kern_ipc_perm asociada.

El hecho de que transformar de identi�cador a recurso sea realizado en un solo punto es unaexcelente invitación para que intervengamos tal punto e intercedamos en este proceso de traducciónde identi�cadores, de modo de encargarse de los tres recursos IPC a cubrir en el mismo lugar. Soloun nuevo hook LSM es necesario y, más allá de que estaríamos modi�cando el kernel, su inclusiónfacilitaría demasiado las cosas.

A pesar de esto, hay otra función general que trata con identi�cadores y estructuras kern_ipc_permy está es ipc_rmid(). A diferencia de ipc_lock(), esta función desasocia un identi�cador IPC de sucorrespondiente recurso y, por ende, también debería ser intervenida para controlar este hecho. Pero,¾por qué?

Si recordamos el enfoque utilizado con los pipes veremos que había un punto en el cual todos loss-hermanos usando una tubería coincidían, y ese era el descriptor de archivo retornado al momentode la creación de la tubería. Luego, el módulo Flowx multiplexaba a partir de este descriptor y realiz-aba operaciones sobre el pipe correspondiente al proceso invocador. Para los IPC System V ocurrirálo mismo; ahora cada identi�cador IPC estará asociado no a uno, sino a varios recursos de diferentenivel. Todos los s-hermanos invocarán las llamadas al sistema sobre los recursos utilizando el mismoidenti�cador y luego, nuestros hooks en ipc_lock() devolverán la estructura kern_ipc_perm que corre-sponda al nivel del proceso que invocó la llamada. Por esta razón, debemos diferir toda liberación deun identi�cador IPC hasta que no haya más recursos �hermanos� compartiéndolo, por lo que interferiripc_rmid() es inminente.

Debemos tener especial cuidado en el momento y la forma en que son invocadas las llamadasal sistema para crear cada IPC. Si dos s-hermanos invocan una apertura o creación de un recurso,debemos tener en cuenta de que ambos deben siempre recibir el mismo identi�cador, siempre y cuandollamen a la llamada al sistema con la misma key. Un problema surge cuando este parámetro contieneel valor IPC_PRIVATE. Para chequear todo esto no nos queda otra alternativa que agregar más hooksen la llamadas al sistema de apertura de cada uno de estos recursos.

Por último, cada recurso IPC System V manipula variables globales que contienen informaciónsobre cada tipo de recurso, así como cada instancia de un recurso particular. Estas variables puedenser consultadas a través de llamadas al sistema que devuelven información sobre mecanismos IPC,como por ejemplo, semctl(). No debemos permitir bajo ninguna circunstancia que un proceso de altonivel modi�que las variables globales que luego procesos de más bajo nivel puede consultar.

A continuación se detallan algunas otras decisiones de diseño especí�cas para cada recurso:

Colas de mensajes

Page 159: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 157

El enfoque para las colas de mensajes System V consiste en crear una cola por cada nivelde proceso que haya abierto una con una key especí�ca.

No hace falta etiquetar a cada mensaje con un nivel de seguridad, ya que este atributoestará dado por la cola en la cual se ubica el mensaje.

Todo proceso de cierto nivel podrá únicamente obtener mensajes de la cola con la mismaclase de seguridad.

Cuando un proceso envíe un mensaje a una cola, el mismo mensaje debe ser replicado a lascolas asociadas al mismo identi�cador pero con nivel más alto, para que procesos de clase deacceso superior puedan acceder también al mensaje. Esto debe hacerse siempre y cuando elproceso no posea un s-hermano con el mismo nivel de la cola donde se replicará en mensaje.

Las variables globales msg_hdrs y msg_bytes almacenan, respectivamente, la suma de losmensajes de todas las colas del sistema y la cantidad de bytes que estos ocupan. Prohibirque procesos de alto nivel manipulen estas variables.

El campo in_use de la estructura ipc_ids descrita en la sección 4.12.3 almacena el númerototal de colas en el sistema. Prohibir que procesos de alto nivel modi�quen este campo.

Semáforos

Se creará un semáforo por nivel de seguridad usando un semáforo IPC con el mismo iden-ti�cador.

Lo único que tienen en común dos niveles de seguridad son los archivos abiertos. Parasincronizar su uso, normalmente se usa �le locking. La creación de s-hermanos no implicaduplicación de memoria por lo que las variables locales de cada uno son diferentes. Nopodemos permitir que un proceso de alto nivel pueda tomar un semáforo de bajo nivel. Másgeneralmente, no podemos permitir que un proceso del nivel que sea bloquee el acceso a unrecurso compartido de nivel menor que él. Esto indudablemente genera un canal encubierto.Por lo tanto, cuando dupliquemos un recurso semáforo dado, solo interferiremos en asignarel semáforo correcto dependiendo el nivel y nada más. No debe haber ninguna interacciónentre semáforos de diferentes niveles.

Al igual que en la cola de mensaje, a variables in_use en la instancia de ipc_ids para eltipo IPC semáforo, almacena el número de semáforos actualmente alojados en el sistema.Procesos de alto nivel no deben poder modi�can este campo.

El campo used_sems en la estructura ipc_namespace contiene la cantidad total de �minisemáforos� alojados entre todos lo semáforos del sistema. Prohibir que lo procesos de altonivel modi�quen este campo.

Memoria compartida

Se creará una nueva región de memoria compartida por cada nivel de seguridad que utiliceun recurso IPC SHM con el mismo identi�cador.

Cada proceso lee de su nivel de región compartida y los procesos de bajo nivel sin s-hermanorespectivo deberán copiar lo escrito en su región en aquellas de más alto nivel.

El problema con este recurso es que una vez insertada la región en el espacio de direccionesde un proceso, el kernel no intercede más en su uso (salvo si hay Fallos de Páginas, versección 4.13). Por lo tanto no podemos permitir el esquema de la Figura 10.5 ya que losproceso de alto nivel podrían perder información útil de bajo nivel.

Para subsanar esto, debemos forzar un esquema como el de la Figura 10.6 para todo grupode procesos que utilicen una región de memoria compartida.

Page 160: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 158

Figura 10.5: C0 y C1 s-hermanos. El subíndice representa el nivel.

Figura 10.6: P0 y P1, C0 y C1 s-hermanos. El subíndice representa el nivel.

Pero lo difícil es que para forzar tal esquema en un momento dado tendremos que pedirle aP0 que se duplique creando un s-hermano apto para escribir en la región. Este es un temadelicado y bastante complicado, ya que las rutinas para duplicar procesos se basan en queel proceso a duplicar es el actual y no un tercero. Por consiguiente debemos encontrar algúnotro mecanismo para satisfacer este requerimiento.

El campo in_use también debe ser protegido contra escrituras de procesos de alto nivel yaque, para regiones de memoria compartida, almacena la cantidad total de segmento alojadospara este tipo de recurso.

El campo shm_tot en la estructura ipc_namespace almacena el número total de páginascompartidas en el sistema a través de este recurso. Su modi�cación debe estar prohibidapara procesos de alto nivel.

Para cada tipo de recurso hay variables y campos de diferentes estructuras que especi�can lacantidad de recursos alojados que puede haber para cierto tipo y cuántos bytes máximos puedenocupar de la memoria. La presencia de estas restricciones introducen un canal encubierto al momentode duplicar recursos con el mismo identi�cador para que sean usados por procesos con otro nivel deseguridad. Por lo tanto, vamos a ignorar las restricciones en estas situaciones y, al mismo tiempo,evitaremos registrar los nuevos recursos duplicados en estas variables globales.

Las implementaciones de cada una las decisiones de diseño introducidas en esta sección serán eltópico del resto del capítulo, centrándose en cada recurso en particular.

Page 161: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 159

10.2.2. Implementación

La implementación del modelo Flowx para cada recurso IPC System V se puede dividir en dospartes. La primera, y general para todos, implementa la intervención de las funciones ipc_lock() eipc_rmid(). La segunda, es relativa a cada recurso y serán desarrolladas en apartados diferentes.

El hook agregado al framework LSM para intervenir ipc_lock() será security_ipc_lock() cuyo pro-totipo será:

struct kern_ipc_perm *security_ipc_lock (struct kern_ipc_perm *);

Como mencionamos en la sección 4.12.3, el campo security de la estructura kern_ipc_perm ofreceun puntero para almacenar datos de seguridad asociados a cada recurso IPC. Existen hooks LSM parainicializar o liberar lo almacenado en este campo y son:

security_sem_alloc() y security_sem_free().

security_msg_queue_alloc() y security_msg_queue_free().

security_shm_alloc() y security_shm_free().

Particularmente, nosotros almacenaremos un instancia de la estructura �owx_ipc:

struct flowx_ipc {

sc sec;

struct list_head other_ipc;

struct semaphore *sem;

struct kern_ipc_perm *my_ipc;

short my_type;

struct list_head shm_list_head;

};

cuyos campos se detallan a continuación:

sec: almacena la clase de seguridad del recurso.

other_ipc: es una lista doblemente enlazada que contiene a los recursos IPC con el mismo iden-ti�cador pero con distinto nivel de seguridad.

my_ipc: contiene un puntero a la estructura kern_ipc_perm que referencia a esta �owx_ipc.

my_type: es un mapa de bits donde cada uno representa el tipo de recurso IPC que contiene estaestructura. Las macros MSG_QUEUE, SEM y SHM describen a cada uno de los bits asignadosa cada recurso.

shm_list_head : es una lista que contiene a todos los procesos que están compartiendo una regiónde memoria compartida. Este campo solo es válido si my_type contiene el bit SHM y su usoparticular será descrito en la sección 10.2.2.3.

La función ipc_lock() recibe un identi�cador y busca en el arreglo de recursos alojados para en-contrar el IPC asociado. Nuestro hook debe intervenir una vez encontrado en recurso pero antes de serdevuelto a la funciones que invocaron a esta función.

La rutina �owx_ipc_lock() es la implementación que provee Flowx para el hook y su tarea consisteen:

1. Obtener la estructura �owx_ipc del recurso argumento.

Page 162: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 160

2. Como el recurso argumento siempre va a ser el del menor nivel, debemos recorrer la lista des-hermanos del proceso actual para crear, para cada uno, un recurso de su nivel.

Si el proceso tiene s-hermanos y no se ha alojado previamente un recurso para su nivel,se invoca a la función create_new_ipc(), la cual usa la información contenida en el campomy_type para saber que tipo de recurso crear. Luego se llama a create_new_msg_queue(),create_new_sem() o create_new_shm().

Un requerimiento en el diseño es que cada nuevo recurso de diferente nivel debería ini-cializarse con el estado de su recurso �hermano� más grande dentro de aquellos con menornivel que él. De esta manera, cuando el proceso de alto nivel empiece a utilizar el recursopuede disponer de toda la información de menor nivel procesada anteriormente.

El mecanismo de copia especí�co de cada recurso será tratado en las secciones correspondi-entes a la implementación de cada uno.

3. Si existe, devolver el IPC asociado al nivel del proceso actual.

4. Puede que un s-hermano haya invocado a ipc_lock() antes de que un hermano haya alojado unrecuso para él. Crear el nuevo recurso, al igual que el item 2 y devolverlo.

Recordemos que el identi�cador asociado a un recurso IPC de nivel 0 servirá de interfaz común paraacceder a todos los recursos hermanos de diferente nivel. El hook security_ipc_rmid() debe interferiren ipc_rmid() para que este identi�cador no pueda ser liberado hasta que toda la familia de recursolo haya sido.

Para solicitar una destrucción de un recurso IPC, se debe invocar a la llamada *ctl(), correspon-diente al recurso, con la solicitud IPC_RMID entre sus parámetros. El framework LSM provee hookspara cada una de estas llamadas, denominados security_sem_semctl(), security_shm_shmctl() y se-curity_msg_msgctl(). Nuestras implementaciones chequean si se ha sido solicitado una destruccióndel recurso y en caso a�rmativo, llaman a la función can_be_deleted(). Esta función debe decidir si sepuede o no seguir con la llamada original, la cual liberará la memoria alojada para el recurso e invocaráa ipc_rmid() para desasociarle su identi�cador.

can_be_deleted() sólo debe permitir liberar a recursos que no sean de nivel 0, ya que este es elpunto el común para toda la familia de IPCs con el mismo identi�cador. Si el recurso es nivel 0 pero notiene hermanos entonces puede ser liberado normalmente. Además puede suceder que tenga hermanospero que estos ya hayan sido marcados para ser borrados y todavía no lo hayan sido. Un recursocon esta característica posee el campo deleted, en su estructura kern_ipc_perm asociada, en 1. Dichorecurso se considera que morirá en cualquier momento y no es tomado más en cuenta como hermano.Este cuidado debe tomarse porque el recurso seguirá perteneciendo a la lista other_ipc de recursoshermanos hasta que sea totalmente destruido.

En el caso de que se invoque la destrucción de un recurso nivel 0 pero este no pueda ser liberadotodavía, igualmente se activa su campo deleted para que futuras aperturas de este recurso fallen. Sino realizasemos esto, como el identi�cador seguirá existiendo, todo proceso de nivel 0 que quiera abrirnuevamente el recurso podrá hacerlo y esto atenta contra la transparencia que buscamos. A partir delmomento en que IPC_RMID es solicitado, el recurso IPC debe mostrarse como destruído para el restode los procesos por más que todavía no lo haya sido.

La función �owx_ipc_rmid() debe intervenir en ipc_rmid() para que contemple tres situaciones:

1. Si el recurso no es de nivel 0, asigna 1 a su campo deleted pero no deja que ipc_rmid() libereel identi�cador. Más tarde se liberará el recurso y su estructura �owx_ipc, pero el identi�cadorseguirá existiendo.

2. Si el recurso es nivel 0 proseguir normalmente. Esto hará que el identi�cador sea liberado. Graciasa can_be_deleted() este caso será pospuesto hasta último momento.

Page 163: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 161

3. Si el recursos no es de nivel 0 y se comprueba que este es el último que queda de la listade recursos hermanos (salvo por el de nivel 0 y aquellos con el campo deleted activado) esmomento de, efectivamente, borrar el recurso 0 y liberar el identi�cador. Para esto se crea unnuevo thread del kernel el cuál, a través de la función ipc_release(), invoca a la llamada al sistema*ctl() para destruir al recurso 0. Esta vez, can_be_deleted() dará paso libre a la destrucción y�owx_ipc_rmid() contemplará el caso 2.

Otro aspecto a implementar es la prohibición de que procesos de alto nivel puedan modi�carvariables o estructuras de bajo nivel que exportan información a espacio de usuario. En la sección dediseño se trató cada una de las variables a proteger y para hacerlo se agrega un nuevo hook al LSM:

int security_ipc_shared_data (struct kern_ipc_perm *);

Cada vez que se este por modi�car algunas de las variables mencionadas se invocará al hook paraotorgar o no permisos.

Por último, notar que debido a que un identi�cador es compartido por varios recursos debemostener especial cuidado cuando diferentes s-hermanos requieren la creación de un nuevo mecanismoIPC. Debemos encontrar un forma para saber cuándo la creación pertenece al mismo pedido en lamisma línea de código de los s-hermanos. Esto es relativamente sencillo si el pedido se realiza usandouna key particular. El problema aparece si se una la key IPC_PRIVATE, ya que dada dos líneas decódigo diferentes con la misma solicitud de creación y con este tipo de valor de key, no podemos saberque identi�cador devolver en cada caso al s-hermano que ejecute último las sentencias.

Por ejemplo, si tenemos el siguiente código:

int main (int argc, char **argv)

{

sem_t id1, id2;

/* Se lee un archivo de alto nivel */

.

.

.

id1 = semget(IPC_PRIVATE, 1, IPC_CREAT | IPC_EXCL);

id2 = semget(IPC_PRIVATE, 1, IPC_CREAT | IPC_EXCL);

.

.

.

return 0;

}

Si el proceso de nivel 0 ejecuta ambas sentencias de creación luego, cuando el s-hermano las ejecute,el kernel deberá devolver para cada identi�cador el que creo previamente el proceso 0. Si no hiciéramosesto, la key IPC_PRIVATE hará que se creen dos recursos semáforos totalmente diferentes a loscreados anteriormente. Nosotros queremos que sean los mismos.

Para lograr cumplir con este requisito se agregan los siguientes hooks a cada una de las llamadasal sistema *get() de cada recurso:

int security_msg_queue_open (key_t, int *);

void security_msg_queue_post_create (key_t, long);

Page 164: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 162

int security_sem_open (key_t, int *);

void security_sem_post_create (key_t, long);

int security_shm_open (key_t, int *);

void security_shm_post_create (key_t, long);

Nuestras implementaciones de los hooks de tipo post_create son invocadas luego de realizar unacreación. Estos crean una instancia de la estructura task_ipc:

struct task_ipc {

long id;

key_t key;

struct list_head ipc;

};

Los campos key e id almacenan la llave y el identi�cador del recurso recientemente creado, respecti-vamente. El campo ipc es una lista para enlazar diferentes estructuras task_ipc. La cabeza de la listadonde se enlazarán estas estructuras se encontrará en el nuevo campo ipc de la �owx_security_structde cada proceso. register_create() enlazará la variable de tipo task_ipc recientemente creada en la listaipc de todos los s-hermanos que posea el proceso actual en ese momento.

Luego, cuando un s-hermano quiera crear un recurso, gracias a los hooks del tipo open (invocadosal principio de toma llamada *get()), se comprobará si no tiene estructuras task_ipc referenciadasen su descriptor de proceso con la misma key que la solicitada. Si la tiene, quita la primera creaciónpendiente de la lista y devuelve el identi�cador almacenado en ésta.

A partir de ahora se detallarán los aspectos de la implementación de cada recurso en particular.

10.2.2.1. Colas de Mensajes

Las rutinas principales que implementan la funcionalidad de las colas de mensajes son las de�nidaspara enviar o recibir mensajes en ellas. Como vimos en la sección 4.12.3.2, la llamada msgsnd() envíaun mensaje a una cola, mientras que msgrcv() obtiene uno.

En cada llamada, el framework LSM de�ne hooks para otorgar o denegar permisos para realizarcada tarea. Nosotros sólo utilizaremos el hook security_msg_queue_msgsnd(), el cual realizará la tareade enviar el mensaje objetivo a todas colas hermanas de niveles mayores de los cuales ya no posea uns-hermano.

Para evitar que un proceso de bajo nivel deba dormir hasta que todas las colas de mensajes dealto nivel sean actualizadas, relegamos este trabajo a un nuevo thread del kernel cuya única funciónes copiar el contenido del mensaje original, elevar su clase de seguridad a la de la cola que quiereactualizar e invocar a la llamada al sistema msgsnd() para que se encargue del resto.

Tanto para las colas de mensajes como para los demás recursos, no debemos intervenir en la familiade llamadas *ctl() (msgctl() para colas de mensajes) cuando lo que se pide no es una destruccióndel recurso. Esto se debe a que gracias a la multiplexación que realizamos en �owx_ipc_lock() paradevolver el recurso IPC que nos conviene partiendo de su identi�cador y el recurso invocador, estasllamadas siempre actuaran sobre el recurso correcto y nunca podrán exportar información que nodeseemos a espacio de usuario.

Un último aspecto de las colas de mensajes es cómo se duplican cuando s-hermanos de diferentesnivel necesitan su respectiva copia. Como se mencionó anteriormente, la función create_new_msg_-queue() se encarga de esto y sus pasos se describen a continuación:

Page 165: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 163

1. Se aloja una nueva cola de mensajes a través de la función ipc_rcu_alloc().

2. Todos los campos de la nueva estructura msg_queue así como los de la estructura kern_ipc_permcontenida en esta última, son inicializados con el mismo valor actual de la cola de nivel 0.

3. Se aloja una nueva estructura �owx_ipc. Su nivel de seguridad es asignado dependiendo el s-hermano que necesita el recurso. Se enlaza la nueva �owx_ipc con sus hermanas y se la almacenaen el campo security de la estructura kern_ipc_perm del nuevo recurso creado.

4. Se busca al recurso hermano más grande de aquellos con menor nivel al nuevo recurso alojado.

5. Se invoca a la función msg_queue_copy() para copiar el contenido de la cola obtenida en el itemanterior con la creada en el item 1. Esta rutina recorre la lista q_messages de mensajes actuales,duplica el mensaje a través de la función msg_copy() y lo inserta en la nueva cola.

6. Retorna la estructura kern_ipc_perm de la nueva cola.

10.2.2.2. Semáforos

No hay mucho más que decir de los semáforos ya que no interferiremos en las rutinas que imple-mentan su funcionalidad.

Solo resta analizar cómo un semáforo es duplicado y qué debemos tener en cuenta en la copia.La rutina create_new_sem() realiza casi lo mismo que su análoga para las colas de mensajes.

Más allá de que nunca un proceso de alto nivel tomará un semáforo de bajo nivel ni viceversa, porcuestiones de usabilidad necesitamos que el nuevo semáforo creado inicie en el mismo estado que suhermano inmediatamente inferior, y efectivamente, a partir de ese momento puedan tomar caminosindependientes.

Un ejemplo de un caso donde no inicializar el semáforo nos traería problemas es cuando un s-hermano se crea luego de que su hermano incrementó el valor de un semáforo, supongamos, a 1. Siinicializáramos el nuevo semáforo en 0, cuando el código intente tomar el semáforo para entrar en unregión crítica, el s-hermano original podrá hacerlo porque previamente había incrementado el valor delsemáforo. En cambio, el nuevo s-hermano dormirá ya que el valor actual de su semáforo asociado en 0y por lo visto en la sección 4.12.3.1 el proceso debe bloquearse.

La función sem_copy() inicializa el valor de cada uno de los �mini semáforos� del recurso reciente-mente alojado con los valores que correspondan.

10.2.2.3. Memoria Compartida

Como ya se destacó en secciones anteriores, las regiones de memoria compartida son el recurso IPCmás poderoso y, por ende, no es de extrañar que sea el más difícil de implementar.

En la parte de diseño vimos que siempre debemos obligar a que, todas las familias de s-hermanosque usan un recuso SHM en particular, posean los mismos niveles de seguridad entre sí. O sea, nopodemos permitir que una familia tenga un s-hermano de cierto nivel mientras que otra familia no. Sino imponemos esta restricción, la información procesada por la segunda familia jamás será procesadaen el recurso SHM del nivel faltante y los otros procesos de este nivel no podrián acceder a datos útilesque deberían poseer por derecho.

Entonces, todos los suscriptores a una región de memoria compartida deben ser noti�cados cuandouna nueva región hermana es creada, de manera que, aquellas familias que no poseen s-hermanos paratratar con dicha región, puedan crearlos.

Además, a diferencia de los otros recursos, duplicar los contenidos de dos regiones de memoria noes una asignatura para nada trivial. Como consecuencia, está sección se dividirá en tres partes:

Cómo registrar a los procesos suscritos a cada región de memoria compartida.

Page 166: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 164

Cómo noti�car a cada proceso que debe duplicarse y cómo realizar esta duplicación.

Cómo duplicar el contenido de una región de memoria compartida.

Registrar procesos a una SHMPara el primer aspecto usamos en hook en la llamada shmat(). En la sección 4.12.3.3 vimos que esta

llamada agrega una región de memoria compartida al espacio de direcciones de un proceso. Recién aquíes válido registrar que un proceso está utilizando la región ya que el hecho de obtener su identi�cadorno signi�ca que haga uso de ella en el futuro.

También hay que cubrir el caso en que un proceso pueda hacer uso de la región sin haber llamadopreviamente a la llamada shmat(). La única forma de que esto ocurra es cuando el proceso hereda laregión de memoria compartida de su padre producto de una llamada al sistema fork().

Por lo tanto, debemos saber:

Qué procesos están registrados a cierta región de memoria compartida

Esto será útil para cuando tengamos que informar a los procesos suscritos a la región que debencrear s-hermanos.

Qué regiones de memoria compartida IPC tiene adjuntas un proceso dado

Esto será útil durante el proceso de creación de un nuevo proceso hijo durante la llamada fork().Para que el item anterior contenga absolutamente a todos los procesos que están usando el áreade memoria compartida, debemos contemplar las situaciones en que están son heredadas. Porende, cada proceso padre debe tener un registro de sus regiones compartidas para que su hijopueda suscribirse a ellas.

La implementación de este requisito se hace a través de la estructura shm_node de�nida como acontinuación:

struct shm_node {

struct task_struct *my_task;

struct list_head shm_list;

struct list_head shm_nodes;

};

Esta estructura simboliza una suscripción a una región de memoria compartida por parte de unproceso. shm_node está diseñada para unir grupos de procesos suscritos a la misma región de memoriay al mismo tiempo regiones de memorias adjuntas a un solo proceso. La Figura 10.7 quizá esclarezcaun poco más esto último.

Page 167: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 165

Figura 10.7: P0, S0 y C0 comparten la misma SHM. P0 está registrado a dos.

La lista shm_list, cuya cabecera se encuentran el campo shm_list_head en la estructura �owx_ipcde cada recurso IPC alojado, cumple la función de unir a todos los procesos registrados a una mismaregión de memoria compartida.

A su vez, la lista shm_nodes, cuya cabecera es el nuevo campo shm_node_head en la estructura�owx_security_struct de cada proceso, cumple la función de unir a todas las regiones de memoriaadjuntas al mismo proceso.

Nuestra implementación del hook security_shm_shmat() aloja e inicializa una nueva estructurashm_node y registra al proceso actual en la región de memoria compartida correspondiente. Luego,enlaza el nuevo nodo creado con los ya existentes en la lista shm_nodes del proceso.

Si la región de memoria es heredada por un proceso hijo, el punto donde intercederemos es en elhook security_task_alloc(). Como vimos en la sección 5.2, en este hook se inicializaba el atributo deseguridad de todo proceso en el sistema, o sea, se alojaba la estructura �owx_security_struct. Aquí esdonde también registraremos al nuevo proceso hijo en cada una de las regiones SHM que heredó de su

Page 168: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 166

padre. Para cada región existente crearemos un nuevo shm_node y la registraremos de la misma formaque en security_shm_shmat().

Uno puede notar que, como un s-hermano también será creado a través de fork(), entonces de lamisma forma se registrará éste a las regiones de memoria compartida de su padre. Esto está bien quesea así ya que todos los s-hermanos utilizan la misma región, más allá de que luego se le sean asignadasdiferentes instancias durante �owx_ipc_lock().

Todos los procesos que comparten una región de memoria compartida IPC (padres e hijos o s-hermanos) deberán registrarse en la región de nivel 0. Como sabemos que esta región será la últimadestruida, es seguro dejar que todos se registren a ella.

Requerir la creación de s-hermanosLa segunda parte de nuestro problema consta en cómo solicitar a un proceso registrado en una

región de memoria compartida que cree un nuevo s-hermano para tratar con una región hermanarecientemente alojada.

Cómo la función do_fork() no puede ser invocada para duplicar un proceso desde otro que no seael actual, debemos encontrar alguna otra forma para realizar esto. Además esta otra forma debe serejecutada antes de que el proceso a duplicarse haga algo, o sea, la duplicación debe llevarse a caboapenas el proceso resume su ejecución.

Cuando un proceso resume su ejecución, luego de que otro invoca un schedule() o cuando unainterrupción �naliza, el kernel realiza un grupo de tarea antes de retornar al nuevo proceso a espaciode usuario. Por ejemplo, una de esas tareas es chequear si el proceso no tiene alguna señal pendienteque le hayan mandado mientras dormía. En caso de tenerlas, el kernel debe ejecutar lo manejadoresde cada una de estas antes de permitirle al proceso resumir. Es más, una señal podría ser SIGKILLmatando al proceso en el acto.

Si basáramos nuestro mecanismo de creación diferida de s-hermanos en el enfoque que utiliza Linuxpara entregar señales a procesos, podríamos usar el código ya de�nido para esto y sólo modi�car unapequeña parte del kernel para lograr nuestro objetivo.

Agregar una señal es muy complicado y presta a dolores de cabeza si, en el futuro se estandarizaalguna nueva señal que posea el mismo número interno que el que nosotros elegimos. Además, el manejode señales no es nada sencillo y existe un enfoque más simple.

Cuando un proceso envía una señal a otro, la bandera TIF_SIGPENDING en el campo �ags dela estructura thread_info del proceso receptor es activada. Tanto la bandera como la estructura estánde�nidas en include/asm-i386/thread_info.h.

Esta variable �ags almacena la información de las tareas pendientes que debe realizar el kernelantes de resumir al proceso actual. El código que chequea estas banderas se encuentra en entry.S yestá íntegramente en assembler. La porción del código que trata con señales es la siguiente:

work_pending:

testb $_TIF_NEED_RESCHED, %cl

jz work_notifysig # Si el proceso no tiene que

# dejar el CPU a otro, fijarse

# si tiene tareas pendientes.

work_resched:

call schedule # Cambia de proceso.

DISABLE_INTERRUPTS(CLBR_ANY)

TRACE_IRQS_OFF

movl TI_flags(%ebp), %ecx

andl $_TIF_WORK_MASK, %ecx

jz restore_all

testb $_TIF_NEED_RESCHED, %cl

Page 169: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 167

jnz work_resched

work_notifysig: # Trata con señales pendientes y

# pedidos de notificación de

# resumen.

movl %esp, %eax

xorl %edx, %edx

call do_notify_resume # Llama a do_notify_resume para

# manejar la entrega de señales

# y/o notificaciones.

jmp resume_userspace_sig # Resume el proceso.

END(work_pending)

La función do_notify_resume() en arch/i386/kernel/signal.c chequea si la bandera TIF_SIG-PENDING está activa para el proceso actual y, si lo está, invoca a la función do_signal(), la cual esel helper principal que maneja la entrega de señales.

Nuestra implementación se basará en utilizar a la rutina do_notify_resume() para ejecutar otratarea adicional diferente a la entrega de señales. Para esto, agregamos en include/asm-i386/thread_info.hlas siguientes de�niciones:

#define TIF_FLOWX 10 /* Un s-hermano debe ser creado */

#define _TIF_FLOWX (1<<TIF_FLOWX)

Luego, modi�camos do_notify_resume() para que además de la bandera TIF_SIGPENDINGchequee si no está activa también nuestra bandera TIF_FLOWX y, en cuyo caso, invoque al nue-vo hook LSM security_interrupt_hook():

void do_notify_resume(struct pt_regs *regs, void *_unused,

__u32 thread_info_flags)

{

/* Pending single-step? */

if (thread_info_flags & _TIF_SINGLESTEP) {

regs->eflags |= TF_MASK;

clear_thread_flag(TIF_SINGLESTEP);

}

/* deal with pending signal delivery */

if (thread_info_flags & (_TIF_SIGPENDING | _TIF_RESTORE_SIGMASK))

do_signal(regs);

//======================== FLOWX ===========================

if (thread_info_flags & _TIF_FLOWX) {

clear_thread_flag(TIF_FLOWX);

security_interrupt_hook();

}

//======================== FLOWX ===========================

clear_thread_flag(TIF_IRET);

}

Antes de ejecutar el hook debemos limpiar la bandera porque no queremos que la rutina secu-rity_interrupt_hook() sea invocada cada vez que el proceso quiera resumir su ejecución y, además,

Page 170: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 168

tampoco queremos que el nuevo proceso tenga esta bandera activa, ya que copy_process() copia casien su totalidad la estructura thread_info. De está forma, cada vez que un proceso desee que otro seduplique creando un s-hermano, solo hay que activar la bandera TIF_FLOWX en la thread_info delproceso objetivo.

Para especi�car qué nivel de s-hermano queremos crear, el nuevo campo request en la estructura�owx_security_struct ofrece un mapa de bits que indica qué solicitudes de creación de s-hermanos haypendientes para el proceso. La rutina �owx_interrupt_hook() es la encargada de crear un s-hermanopor cada bit activo en request e inicializando el nivel de acuerdo a la posición del bit en la variable. Comoesta función se ejecuta con la interrupciones deshabilitadas, debemos activarlas nuevamente, ya quetodas las rutinas invocadas para crear s-hermanos necesitan dormir para obtener memoria o accedera ciertas estructuras de datos. Del mismo modo, una vez terminada la creación, debemos habilitarnuevamente las interrupciones para que el kernel �nalice exitosamente el cambio de contexto que seoriginó antes de satisfacer nuestro pedido a través de la bandera TIF_FLOWX. Las rutinas del kernellocal_irq_disable() y local_irq_enable() son de utilidad para activar o desactivar las interrupcionescuando sea necesario.

Duplicar una SHMPor último, detallemos cómo una región de memoria compartida IPC es duplicada para que un

proceso de alto nivel pueda usarla y cómo los procesos suscritos a la región serán noti�cados de estenuevo evento.

Al igual que los otros recursos IPC System V, la función create_new_shm() aloja una nueva regiónde memoria compartida e inicializa los campos de las estructuras relacionadas con lo mismo valoresque las estructuras de la región original. Sin embargo, como vimos en la sección 4.12.3.3, cada regiónestá asociada a un objeto �le registrado al sistema de archivo shm. Este objeto sirve para crear unmapeo de memoria en el espacio de direcciones del proceso.

Nuestra nueva región debe contener un nuevo archivo, ya que de lo contrario no podríamos evitarla compartición de los datos entre diferentes niveles. Para esto, usamos la rutina shmem_�le_setup()para alojar un nuevo archivo en el sistema de archivos shm y lo almacenamos en el campo shm_�lede la estructura shmid_kernel asociada al nuevo recurso.

Para copiar los contenidos de cada región, lo más sencillo es copiar página por página hasta duplicarla totalidad de los datos. Puede que la región tenga un tamaño más grande que los datos realmenteútiles a copiar.

La rutina shm_copy() utiliza la función i_size_read() para obtener el tamaño del inodo de laregión a copiar, obtiene referencias a los inodos de ambas regiones involucradas e invoca a la funciónmmap_copy() para copiar los contenidos de la memoria. mmap_copy() copia entre dos mapeos dememorias genéricos (como se detalló en la sección 4.13.4.1 hay diferentes tipos de mapeos), y seráexplicada más profundamente en la sección 10.3. También en esa sección se explicará que ocurrecuando se crea un s-hermano y este hereda una región de memoria compartida, sea del tipo IPC o no.

Para �nalizar, para satisfacer el requisito de alertar a los procesos suscritos de que un nuevo recursoSHM ha sido creado, create_new_shm() invoca a la función duplicate_subscribers(). Esta rutina deberecorrer la lista de suscriptores a la región y solicitar una creación de s-hermanos a cada integrante dela lista siempre y cuando ya no lo tenga. Su código se incluye a continuación:

static void duplicate_subscribers(struct flowx_ipc *fipc, sc *sc)

{

struct shm_node *pos;

for_each_shm_subscriber(fipc, pos) {

struct task_struct *target = pos->my_task;

struct flowx_security_struct *fxs = target->security;

Page 171: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 169

if (find_s_sibling(fxs, sc))

continue;

if (s_sib_creation_requested(fxs, sc))

continue;

add_request(fxs, sc_getlevel(sc));

set_ti_thread_flag(target->thread_info, TIF_FLOWX);

}

}

Como mencionamos anteriormente, todos los procesos, s-hermanos o no, se registran en el recursoSHM de nivel 0. Entonces, como durante la iteración de suscriptores de la lista podemos encontrarnoscon procesos s-hermanos entre sí, no tiene sentido pedirle a cada uno que se duplique; solo basta conuno. La función s_sib_creation_requested() chequea si la solicitud de creación ya ha sido expedidapara algún s-hermano del proceso, caso en el cual simplemente no se hace nada.

La decisión de registrar a todos los procesos, sin importar el nivel, en el mismo recurso puedeexplicarse, paradójicamente, a partir de la siguiente pregunta: ¾qué ocurriría si el proceso de nivel 0muriese pero no sus s-hermanos? Si, por ejemplo, duplicate_subscribers() quisiera crear un s-hermanonivel 2, su hermano nivel 0 no existiese más, pero si el de nivel 1, ¾de qué forma se le avisaría alproceso 1 que debe duplicarse? Una respuesta inmediata sería: si registráramos cada proceso a sucorrespondiente recurso, duplicate_subscribers() debería iterar no solo sólo sobre suscriptores, si notambién sobre recursos hermanos (lista other_ipc) para obtener a todos los procesos que deben sernoti�cados. Con este enfoque, también, debería modi�carse el proceso de suscripción de procesos yaque fork() no tiene en cuenta si el proceso duplicado será simplemente hijo del actual o un s-hermano.Por todo esto, decidimos que era mucho más sencillo adoptar el enfoque utilizado y no este último.

Un último aspecto a detallar con respecto a cuándo duplicar un región de memoria compartida esqué sucede cuando se crea un s-hermano que heredará regiones de su creador. Obviamente no podemospermitir que esto ocurra ya que habría un canal de comunicación entre procesos con accesos a diferentesniveles de información. Sin embargo, la implementación de este aspecto abarca no solo a los recursosIPC SHM, sino también, a los mapeos de archivo en memoria. Por lo tanto, se deja esta explicaciónpara la sección siguiente en donde se tratan estos mecanismos del kernel.

10.3. Mapeo de Archivos

Más allá de que los mapeos de archivos en memoria descriptos en la sección 4.13.4.1 no son con-siderados explícitamente como un mecanismo de comunicación entre procesos IPC, según lo visto enlas sección 10.2.2.3 de la memoria compartida, el kernel utiliza el enfoque de mapear un archivo sinimagen en disco para ligar una región SHM con el VFS. Por lo tanto, es válido tratar el tema de cómoel módulo Flowx asegura un mapeo seguro de archivos en este capítulo.

10.3.1. Diseño

En la sección anterior observamos que el módulo debía crear nuevas copias de regiones de memoriacompartida para cada nivel de seguridad que la este utilizando en un momento dado. Duplicar unaregión se debía llevar a cabo en dos situaciones:

Cuando un proceso de nivel mayor a 0 adjuntaba una región a sus espacio de direcciones (llamadashmat()).

Cuando un proceso con una región SHM adjunta creaba un nuevo s-hermano.

Page 172: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 170

De esta forma, nunca procesos con diferentes niveles de seguridad podrían compartir una región dememoria por lo cual se asegura que compartir datos sensibles a través de este canal sea imposible. Sinembargo, hay otra forma en la que procesos de distinta clase de acceso pueden compartir regiones dememoria: el mapeo de archivos.

En la sección 4.13.4.1 vimos que un mapeo de archivo en memoria puede categorizarse en tres tiposdiferentes:

Mapeo Compartido

Mapea un archivo de disco en la memoria. El mapeo puede ser accedido por varios procesos yasí compartir la información que pertenezca al archivo.

Otra forma de mapeo compartido es a través de una región de memoria SHM. En este caso, elarchivo no tiene su correspondiente imagen en disco, sino que se crea un archivo especial en elsistema de �cheros shm.

Mapeo Anónimo

Mapea un archivo sin imagen en disco. Puede ser compartido o no.

Mapeo Privado

Se crea una copia del archivo en disco a través de mecanismo Copy On Write (ver sección 4.13.5.1).Cada modi�cación en la región no modi�ca el archivo original.

Nosotros solo intervendremos en el mapeo compartido y el anónimo compartido. Los puntos de inter-vención, al igual que el mecanismo IPC SHM, serán durante la llamada mmap2() y durante la creaciónde s-hermanos.

Si observamos detenidamente, un mapeo de archivo es un mecanismo más directo de acceder a unarchivo sin tener que pasar por las llamadas al sistema del VFS para hacerlo. Por lo tanto, tendríamosque aplicar las mismas políticas de seguridad impuestas en el capítulo 5. Sean P y S dos procesos delsistema con clases cP y cS , respectivamente, tal que cP ≺ cS . Sean A y B dos archivos con clases cA ycB, respectivamente, tal que

cA ≺ cB

cA = cP

cB = cS

entonces:

Si cP mapea a cA

Permitir el mapeo.

Si cS mapea a cB

Permitir el mapeo.

Si cP mapea a cB

Primero deberá abrirlo para luego accederlo y mapearlo. La intervenciones en el capítulo 7 yanos ofrecen una protección para esto, siempre y cuando se ubiquen archivos de alto nivel endirectorios de alto nivel. Para cuando esta regla no se cumpla:

Abrir el archivo /dev/zero1.

1El dispositivo /dev/zero desecha toda escritura sobre él y devuelve ceros por la cantidad requerida en una lectura.

Page 173: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 171

Mapear /dev/zero.

Retornar de mmap2() con éxito.

De esta forma, el proceso de bajo nivel no mapeará directamente al de alto nivel y toda escriturao lectura en la región de memoria mapeada modi�cará el dispositivo /dev/zero.

Si cS mapea a cA

No permitir que un proceso de alto nivel mapee en memoria un archivo de bajo nivel, ya que delo contrario no podremos intervenir en la escrituras al archivo. Entonces:

Crear un nuevo archivo pero que no tenga una imagen en disco.

Copiar en el nuevo archivo el contenido del archivo a mapear originalmente.

Realizar el mapeo con el nuevo archivo.

De esta forma, el proceso de alto nivel podrá acceder a los datos del archivo original a mapear ymanipular estos datos. Sin embargo, la modi�cación de información no tendrá efecto alguno enel archivo y, por ende, ninguna violación en la seguridad ocurre. Un problema de compatibilidadocurre si un proceso de bajo nivel modi�ca el archivo original mientras uno de alto nivel lo tienemapeado. En este caso, ocurrirá que el proceso de alto nivel tendrá una copia desactualizadadel archivo. A pesar de esto, como consideramos que el mapeo de archivos no tendría que serun mecanismo de comunicación entre procesos obviamos esta restricción. Si un programa deseaque se vuelquen diferentes niveles de información en el mismo archivo, entonces el programa esconsiderado muy inseguro y no nos importa que no ande bajo nuestro módulo.

Si otro proceso de nivel igual a S quisiera mapear a A en el futuro, debe mapear la misma copiacreada para S ya que ahora, si es correcto que el nuevo proceso reciba un mapeo actualizado delarchivo, pero actualizado solo por procesos de su mismo nivel.

Teniendo en cuenta estas especi�caciones la sección siguiente detallará como se implementó elmapeo de archivo seguro del módulo Flowx.

10.3.2. Implementación

Cuando un proceso invoca a la llamada al sistema mmap2() podemos de�nir nuestra propia imple-mentación del hook LSM security_�le_mmap() para imponer el diseño detallado anteriormente. Sinembargo, no es el único punto donde debemos aplicar nuestra política de seguridad, ya que durante lacreación de s-hermanos tampoco debemos permitir que el nuevo hermano herede un región de memoriaque mapee a un archivo que no pueda modi�car.

La función �owx_�le_mmap() solo actúa si estamos en presencia de un mapeo compartido oanónimo compartido. Como este hook también es invocado cuando se aloja una región de memoriacompartida IPC y los mecanismos de control para este tipo de recurso son diferentes que para invoca-ciones directas a la llamada mmap2(), debemos evitar intervenir bajo estos casos. Para detectar queun archivo a mapear pertenece a una región SHM IPC basta con comprobar que el campo type en laestructura �owx_inode_struct del inodo asociado al archivo no contenga la bandera I_SMH activada.

Una vez que �owx_�le_mmap() detecta que el archivo no pertenece a una región de memoriacompartida IPC y que la clase de acceso del proceso actual y el archivo son diferentes:

1. Si el proceso desea mapear un archivo mayor a él

Crea un nuevo archivo con la función shmem_�le_setup().

Copia el contenido del archivo original en el recientemente creado a través de la funciónmmap_copy() descrita en la sección 4.12.3.3.

Page 174: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 172

2. Si el proceso desea mapear un archivo menor a él

Abre el archivo /dev/zero con la rutina �lp_open().

Crea un nuevo s-hermano para mapear al archivo original de mayor nivel.

3. Se crea una estructura de tipo �owx_mmap:

struct flowx_mmap {

struct file *mmap_file;

struct list_head files;

};

Esta estructura se almacenará en el campo security_data de la estructura �owx_inode_structdel inodo asociado al archivo a mapear y contendrá los archivos de cada nivel creados en lositems anteriores. Cuando un proceso quiera mapear un archivo ya mapeado por otro deberárecorrer la lista para ver si ya no se ha creado una imagen correspondiente para su nivel. Encaso a�rmativo, se obvian los dos primeros items y se devuelve el archivo encontrado. Paradistinguir entre un archivo mapeado en memoria y otro que no lo está, se activa el bit I_MMAPen el campo type. Durante �owx_inode_free(), si el archivo corresponde a un mapeo se invocaa �owx_mmap_free() para remover al archivo de la lista y liberar la memoria de la estructura�owx_mmap asociada a éste.

4. Reemplaza el puntero al archivo a mapear argumento, por el nuevo. Para realizar esto se tuvoque modi�car el hook LSM original para que adopte el siguiente prototipo:

int security_file_mmap (struct file **file, unsigned long req_prot,

unsigned long prot, unsigned long flags);

La función mmap_copy() es utilizada tanto para copiar entre páginas de memoria compartida IPCcomo páginas entre mapeos de archivo regulares de disco. Si recordamos los conceptos de la sección4.13.8.1, los contenidos de estas páginas son respaldados en diferentes ubicaciones en el momento enque el kernel reclama sus marcos de memoria. Para copiar página a página dos regiones de memoriasdistintas, mmap_copy() debe alojar un marco de memoria para cada página y actualizar su contenido.Sin embargo, debemos diferenciar entre los tipos de regiones, ya que las rutinas provistas para alojarmarcos para una página en la unidad swap no son las mismas que para páginas pertenecientes a unarchivo de disco. Los mapeos a archivos regulares siempre proveen un método denominado readpage()en el campo a_ops de su estructura address_space. La función read_cache_page() obtiene una páginaactualizada de la cache de páginas. Si, particularmente, la página no está en la cache o está, pero noactualizada, read_cache_page() utiliza el readpage() para lograr su cometido.

Las páginas en la unidad swap normalmente no proveen un método readpage() en sus mapeos.Cuando una página de estas características no se encuentra en memoria física, o se encuentra, perono actualizada, hay que usar rutinas especiales para obtener el contenido de la página de la unidad deintercambio swap. La función shmem_getpage() realiza todo este trabajo por nosotros.

Además del tipo de páginas a copiar, mmap_copy() debe tener en cuenta donde se encuentranestas páginas, ya que si lo están en memoria alta (ver sección 4.13.3) se deben mapear al espacio dedirecciones del kernel para ser usadas. La macro PageHighMem() nos ayuda a determinar si una páginaestá o no en memoria alta y, las funciones kmap_atomic() y kunmap_atomic(), nos permiten realizarlos mapeos sin bloquear a los procesos solicitantes.

Una vez contemplados los detalles descritos, se invoca a un simple memcpy() para copiar entre lasdos direcciones físicas obtenidas para cada página.

Page 175: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 173

Herencia de MapeosEs hora de explicar cómo procede el módulo cuando un s-hermano debe heredar una región de

memoria compartida de su hermano creador. La región es siempre producto de un mapeo de archivos,pero este puede ser un mapeo explícito debido a una invocación de mmap2() o a través de un solicitudde una nuevo región de memoria compartida IPC. Para comprender totalmente la implementación deeste requerimiento, se recomienda leer la sección 4.13 sobre la memoria en Linux.

A la hora de crear un nuevo s-hermano, la función create_new_s_sibling() llama a check_new_-mm() para recorrer cada una de la regiones de memoria del proceso y detectar cuáles de éstas soncompartidas. Básicamente, check_new_mm() realiza las siguientes operaciones:

1. Obtiene el descriptor de memoria de proceso actual referenciado en el campo mm de la task_-struct.

2. Itera sobre la lista de regiones de memoria (VMAs) cuya cabecera se encuentra en el campommap del descriptor obtenido en el item 1.

3. Para cada región comprueba si es compartida o no, chequeando si el campo vm_�ags tenga o nola bandera VM_SHARED activada.

4. Determina el tipo de mapeo en la región. Para esto utiliza el campo type de la estructura�owx_inode_struct del inodo asociado al archivo mapeado. Este se encuentra el campo vm_�le.Dependiendo del tipo de mapeo realiza:

SHM IPC

Crea una nueva región de memoria compartida a través de la función create_new_shm()descrita en 4.12.3.3.Crea un nuevo archivo para asociarlo a la nueva región creada.Agrega la VMA en el árbol de búsqueda por prioridad del nuevo archivo. De esta formase podrá realizar un correcto mapeo en reversa cuando se quieran liberar los marcospertenecientes a la nueva región (ver sección 4.13.8.2).Remueve la VMA del árbol de búsqueda por prioridad del archivo anteriormente ma-peado.

Mapeos de archivos o anónimos

Comprueba si el archivo mapeado en la región ya no posee una estructura �owx_mmapasociada y, en caso a�rmativo, busca si no existe algún archivo del nivel del s-hermanoa crear.Si no hay un archivo para el nuevo mapeo, crearlo a través de la función shmem_-zero_setup(). Esta función reemplaza el archivo creado con el referenciado en el campovm_�le de la región que recibe como argumento. Luego copia en el nuevo archivo elcontenido del archivo originalmente mapeado. Por último crea una nueva estructura�owx_mmap y la registra al inodo.Agrega la VMA en el árbol de búsqueda por prioridad del nuevo archivo.Remueve la VMA del árbol de búsqueda por prioridad del archivo anteriormente ma-peado.

5. Limpia todas las entradas de la Tabla de Páginas que corresponden a la VMA recientementemodi�cada. De esta forma, cada vez que el proceso referencie alguna dirección de memoria aso-ciada a la región, un Fallo de Página tendrá efecto y se invocará a la rutina nopage() para alojarun nuevo marco de memoria para la página e inicializar su contenido. Además toda entrada de laTLB debe ser removidas para cada página en la VMA. Si no realizamos esto, el nuevo s-hermanopodría seguir utilizando direcciones lineales que no generan fallos (y por lo tanto no intervieneel kernel) y a las cuales no debería tener más permisos de acceso.

Page 176: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 174

10.4. Colas de Mensajes POSIX

Esta última sección tratará tanto el diseño como los detalles de implementación de controles deseguridad para las colas de mensajes POSIX.

El requerimiento para las colas de mensajes POSIX no varía mucho del de sus análogas System V.Básicamente, lo que se requiere es que haya una cola por nivel de seguridad utilizando el recurso, quelas colas de mayor nivel vayan siendo actualizadas con los contenidos de sus hermanas de menor nively que un proceso de cierta clase de acceso pueda obtener mensajes de una y solo una cola dentro deuna familia de colas POSIX.

Si recordamos los detalles explicados en la sección 4.12.4, cada cola de mensajes es representadapor el descriptor mqueue_inode_info. Esta estructura embebe a un inodo alojado en el sistema dearchivos mqueue.

Todas las llamadas al sistema provistas para crear y/o manipular colas de mensajes POSIX tienencomo argumento un puntero al objeto �le representando una cola y luego, invocan a la funciónMQUEUE_I() para obtener la estructura mqueue_inode_info asociada al �le:

struct mqueue_inode_info *MQUEUE_I(struct inode *inode)

{

return container_of(inode, struct mqueue_inode_info, vfs_inode);

}

Este es un buen punto para intervenir ya que, a partir de ahora, toda tarea que realicen las llamadas sellevarán a cabo a partir del descriptor de la cola de mensajes obtenido. Por lo tanto, reemplazaremostodas las instancias de código donde aparecen las siguientes sentencias (en las llamadas que interveng-amos):

struct mqueue_inode_info *info = MQUEUE_I(filp->f_path.dentry->d_inode);

por estas otras:

struct mqueue_inode_info *info;

info = security_posix_mqueue_security(filp->f_dentry->d_inode, filp,

LSM_MQUEUE_GETSET);

if (!info)

return -EPERM;

Como las colas de mensajes POSIX fueron incluidas en el kernel 2.6 después que el frameworkLSM, no hay hooks que contemplen este nuevo mecanismo de comunicación entre procesos. Parapoder intervenir correctamente en la seguridad de estos recursos, tuvimos que agregar nuevos hooks alframework. Particularmente, security_posix_mqueue_security() es uno y es el encargado de realizarla multiplexación de colas de mensajes a partir del objeto �le alojado tras una llamadas mq_open().

El tercer campo del hook es un mapa de bits que indica el contexto de la invocación. En pocaspalabras, cada bit del mapa determina cuál llamada al sistema requirió la multiplexación. Los valoresposibles son:

/* En sys_mq_timedsend() */

#define LSM_MQUEUE_SEND 1

/* En sys_mq_timedreceive() */

#define LSM_MQUEUE_REC 2

/* En sys_mq_getsetattr() */

Page 177: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 175

#define LSM_MQUEUE_GETSET 4

/* En sys_mq_notify() */

#define LSM_MQUEUE_NOTIFY 8

/* En mqueue_poll_file() */

#define LSM_MQUEUE_POLL 16

/* En mqueue_destroy_inode() */

#define LSM_MQUEUE_DESTROY 32

Para todas las llamadas menos destroy(), nuestra implementación, aparte de devolver la cola demensajes asociada al nivel del proceso a través de la función get_mqueue_info(), también chequea siel proceso actual posee s-hermanos y, para cada uno de éstos, crea un nueva cola a través de la funcióncreate_new_mqueue(). Además almacena, para cada cola que invoque estas llamadas, un descriptordel tipo �owx_mq en el campo security_data de la estructura �owx_inode_struct alojada en el inodoasociado.

struct flowx_mq {

struct mqueue_inode_info *info;

struct list_head other_mq;

sc sec;

};

El nivel de cada cola estará especi�cado por el campo sec de la estructura �owx_mq y el campoother_mq será de utilidad para enlazar familias de colas de mensajes que compartan el mismo inodode entrada.

Una vez que creada una cola de mensajes (o no, si ya existe), el hook retorna la estructuramqueue_inode_info que corresponda al nivel del proceso invocador. De esta maneara, todas estasllamadas al sistema llevarán a cabo sus tareas en colas diferentes dependiendo el nivel del procesoactual.

Queda un último canal de información a cubrir y es aquel que surge al actualizar el tiempo deúltimo acceso al inodo asociado a la cola. Como este inodo es común para toda las familia de colas,sólo el proceso con el mismo nivel del inodo puede actualizar dicha información. Para esto, se agregóel hook security_posix_mqueue_inode_time() al framework LSM.

Tener en cuenta que, a diferencia de los recursos IPC System V, el punto en común para todala familia de colas de mensajes POSIX (el inodo) no tiene por qué ser de nivel 0. Será del nivel dels-hermano que invoque primero a mq_open().

Para satisfacer el requerimiento de actualizar colas de más alto nivel cuando un proceso no tengaun s-hermano para realizar el trabajo, se agrega el hook security_posix_mqueue_post_send(). Estafunción debe actuar una vez que un mensaje haya sido exitosamente insertado en un cola y realiza losiguiente:

1. Recorre la lista de colas de mensajes hermanas.

2. Si encuentra un cola de nivel superior, utiliza la función can_write() (descrita en la sección 5.6)para determinar si debe duplicar el mensaje en la cola o no.

3. Duplica el mensaje con la rutina msg_copy().

4. Determina si la cola a actualizar está llena o no.

Si está llena, crea un nuevo thread del kernel para realizar la tarea de actualizar la cola. Elargumento del thread será una estructura del tipo thread_request :

Page 178: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 176

struct thread_request {

struct file *filp;

struct msg_msg *msg;

sc sec;

};

Este argumento detallará el archivo asociado a la familia de colas, el nuevo nivel de seguridadal que el thread se debe cambiar para actualizar la cola especí�ca y el mensaje a insertar.

El nuevo thread invocará nuevamente la llamada mq_timedsend(), pero ahora el nivel delproceso será el de la cola objetivo y la solicitud se completará exitosamente.

Si no está llena, el proceso actual almacena el mensaje copiado.

5. Si se creó un thread para completar la solicitud, retorna inmediatamente. Esto se debe a que,como el thread ejecutará nuevamente mq_timedsend(), este hook se ejecutará de nuevo y, de estemodo, se actualizarán recursivamente las colas mayores que resten.

Para abrir o crear una cola de mensajes POSIX se utiliza la llamada al sistema mq_open(). Paraindicar una creación en caso de que la cola no exista, se debe especi�car la bandera O_CREAT enlos argumentos de la llamada. La bandera O_EXCL, junto con O_CREAT, indica que nada más meinteresa crear un cola nueva y no abrir una existente.

Nuestra preocupación radica en qué realizar cuando dos s-hermanos diferentes intentan abrir lamisma cola y ambos especi�can las banderas O_CREAT y O_EXCL para hacerlo. Si no intervenimosen esta situación, el segundo s-hermano que intente crear la cola no lo hará porque su hermanoya lo habrá hecho. Por lo tanto, agregamos el hook security_posix_mqueue_open() con el siguienteprototipo:

int security_posix_mqueue_open (struct dentry *dentry, int *oflag)

Esta función será invocada antes de que mq_open() chequee las banderas en el argumento o�ag. Comopuede observarse, el hook recibe un puntero a estas banderas para poder modi�carlas si así lo requiriese.Particularmente, la función �owx_posix_mqueue_open() chequeará:

Si la cola ha sido creada. Para esto comprueba si el inodo al que referencia el objeto dentryargumento no es NULL.

Si las banderas O_CREAT y O_EXCL están especi�cadas en o�ag.

Si los dos primeros items se cumplen, itera sobre la lista de s-hermanos para determinar si algunoes el dueño de la cola de mensajes a abrir.

En caso de que se cumplan todos estos items, la función simplemente modi�cará el argumento o�agspara que no contenga la bandera O_EXCL, y así permitir al s-hermano que abra la cola aunque otroya la haya creado. El siguiente código ilustra esto último:

int flowx_posix_mqueue_open (struct dentry *dentry, int *oflag)

{

struct flowx_security_struct *current_fxs = current->security;

struct inode *inode = dentry->d_inode;

if (!current_fxs) {

current_fxs = flowx_security_struct_init();

if (!current_fxs)

return -ENOMEM;

current_fxs->my_task = current;

Page 179: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 10. COMUNICACIÓN ENTRE PROCESOS 177

current->security = current_fxs;

}

if (inode && (*oflag & O_CREAT) && (*oflag & O_EXCL)) {

struct mqueue_inode_info *info = MQUEUE_I(inode);

struct flowx_security_struct *pos;

for_each_s_sibling(current_fxs, pos)

if (pos->my_task->uid == info->user->uid)

*oflag = *oflag & ~O_EXCL;

}

return 0;

}

Por último describamos cómo es eliminada una familia de colas de mensajes POSIX en el móduloFlowx. Como vimos en la sección 4.12.4, para destruir una cola es necesario invocar a la llamadamq_unlink(). Esto hará que el nombre de la cola sea removido inmediatamente, pero la cola en sí,seguirá existiendo en el kernel hasta que todos los procesos usándola cierren los descriptores de archivoque la referencian.

Cada vez que un s-hermano es creado, hereda de su creador sus descriptores de archivos abiertos y,por ende, los conteos de referencia de estos últimos son incrementados en uno. Esto nos garantiza que,más allá de que un proceso particular emita un mq_unlink(), el inodo asociado a la familia de colas noserá destruido hasta que todos los s-hermanos cierren sus respectivos recursos. Cuando esto suceda,el conteo de referencia del inodo llegará a cero y, eventualmente, el hook inode_free_security() seráinvocado. En �owx_inode_free_security() debemos tener en cuenta todo esto y llamar a una funciónespecial para liberar a todas las estructuras �owx_mq asociadas a la familia de colas. Esta función,llamada free_all_mqueues(), recibe una cola e itera a través de la lista other_mq para ir liberando atodas las estructuras hermanas. Recordemos que el campo type del inodo indicaba que este pertenecía auna cola de mensajes POSIX y la cola argumento que recibe free_all_mqueues() se obtiene del camposecurity_data de la estructura �owx_inode_struct asociada al inodo en cuestión.

Page 180: Flowx: Implementación de no interferencia en Linux

Capítulo 11

Conclusiones

En sistemas militares la seguridad requiere que los procesos no puedan transferir datos desdearchivos de seguridad alta a archivos de seguridad baja; pero no solo se debería prohibir leer undocumento con�dencial, sino también controlar que no se tenga acceso a su contenido a través de lacolaboración, por caminos muy ingeniosos, con usuarios que tengan la autorización para acceder a taldocumento. En este trabajo presentamos el prototipo Flowx, el cual cumple con este requerimientoimponiendo políticas de seguridad a través de controles dinámicos que satisfacen propiedades de nointerferencia.

La implementación del prototipo se basó completamente en el uso de la tecnología de los Lin-ux Security Modules (LSM). El uso de este framework resultó una experiencia totalmente positiva ysatisfactoria. Las ventajas principales pueden ser vistas en términos de organización, e�ciencia y porta-bilidad. Organización, porque tenemos más del 90% del código de Flowx en un módulo de seguridaddel kernel. Los cambios en el árbol principal de fuentes fueron los mínimos e indispensables, y cualquiermodi�cación futura tendría, solamente, que modi�car algunas de las implementaciones especí�cas delos hooks usados. De esta manera, estas modi�caciones no repercutirían tanto como si el kernel tuvieraque ser parcheado nuevamente. Además, se puede reconocer claramente los puntos donde el móduloactúa y no �surfear� o recorrer todo el núcleo para identi�car los controles de seguridad que se esténimponiendo.

La e�ciencia recae principalmente en el testeo y debugeo del módulo en etapa de desarrollo. Trabajardirectamente con el kernel hubiera implicado recompilarlo cada vez que el mínimo cambio sea efectuado,hasta el más pequeño printk() para imprimir algún mensaje útil. Gracias al framework, hubo querecompilar, muchas veces, solo el módulo y dejar intacto el núcleo an�trión.

La portabilidad se basa en que los mantenedores de LSM serán los que porten el framework a futurasversiones del kernel 2.6. Lo único que nosotros deberíamos realizar es chequear que los hooks usadospor nosotros todavía existan y que se sigan encontrando en las ubicaciones que nosotros pretendemos.

Evaluemos ahora el funcionamiento del prototipo y para esto, analicemos las tres propiedades quenos propusimos al inicio de este trabajo, o sea, la seguridad, la usabilidad y la compatibilidad conel software existente. Los controles para la lectura y escritura protegen al sistema contra los ataquescomunes de caballos de Troya que asumen la identidad de un usuario determinado. Sin embargo, elconcepto del s-hermano es la esencia del modelo y lo que hace a nuestro sistema invulnerable contrauna gran variedad de ataques basados en canales encubiertos. Los canales probabilísticos y temporalesson muy difíciles tanto para explotar como para proteger y el modelo no ofrece protección contra ellos.

Con los controles de Flowx activados, el sistema es muy usable y el rendimiento es casi el mismoque en situaciones normales. La entrada compartida entre los procesos con diferentes niveles en unasesión provee una noción de transparencia al usuario �nal con respecto a la existencia de s-hermanosen ejecución y si hay controles de seguridad imponiéndose.

Cabe destacar que el sistema no será del todo compatible con todas la aplicaciones estándares deLinux, pero variando levemente la forma en que son ejecutadas, se puede lograr un nivel de compati-

178

Page 181: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 11. CONCLUSIONES 179

bilidad aceptable. Un ejemplo claro es el editor de texto Vim. Este editor usa numerosos archivos paraguardar información de historial, sincronizar accesos concurrentes de escritura sobre un archivo dado,etc. En este último caso surgen problemas cuando dos s-hermanos quieren abrir separadamente unarchivo particular. Supongamos que este archivo se denomina foo. Apenas el primer Vim abra foo, secreará el �chero .foo.swp. Cualquier otro Vim que intente abrir foo chequeará la existencia de .foo.swpy, en caso que lo encuentre, se quejará por querer intentar un acceso concurrente sobre el archivo. Ob-viamente, no queremos que esto ocurra, ya que podría, en un momento dado, divulgar de la presenciade un s-hermano o hacer que Vim produzca errores varios que afecten considerablemente la usabilidadde este programa. Para solucionar esto, invocamos a Vim con su opción -n, que hace que no se creenarchivos .swp, y su opción -i NONE, que no usa un archivo para guardar el historial al ejecutarlo deesta forma.

Como este, hay otros casos donde debemos mediar en la invocación de programas de usuario paraque su ejecución (a veces un tanto rebuscada) se adecue con nuestro módulo. Simplemente creamosscripts �wrappers� que envuelvan la invocación a un programa particular, agregándoles las opciones delínea de comando que nosotros consideramos necesarias para que la aplicación no se queje.

Los testeos y debugeo del módulo fueron realizados sobre un sistema Linux creado especialmentepor nosotros. Gracias al proyecto Linux From Scratch1 (LFS), el cual consiste en enseñar a usuarios deeste sistema operativo cómo crear su propia distribución, pudimos compilar e instalar todo el softwarebásico para correr nuestro módulo y no tener que adaptarnos a una distribución especí�ca.

Para �nalizar, el cuadro 11.1 detalla, a modo de resumen, los hooks utilizados por el prototipo ylas secciones donde se trató cada uno.

1www.linuxfromscratch.org

Page 182: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 11. CONCLUSIONES 180

Capítulo 5 �owx_posix_mqueue_security() N�owx_inode_alloc_security() H Pipes�owx_inode_free_security() H �owx_�le_free_security() H�owx_inode_init_security() H System V IPC

�owx_inode_setxattr() H �owx_ipc_lock() N�owx_inode_post_setxattr() H �owx_ipc_rmid() N

�owx_inode_setattr() H �owx_ipc_shared_data() N�owx_task_alloc_security() H Colas de Mensajes�owx_task_free_security() H �owx_msg_queue_open() N�owx_�le_permission() H �owx_msg_queue_post_create() N

Capítulo 6 �owx_msg_queue_msgsnd() H�owx_�le_ioctl() H �owx_msg_queue_msgctl() H�owx_�le_select() N �owx_msg_queue_alloc_security() H

Capítulo 7 �owx_msg_queue_free_security() H�owx_inode_create() H Semáforos�owx_inode_mkdir() H �owx_sem_open() N�owx_inode_rmdir() H �owx_sem_post_create() N�owx_inode_unlink() H �owx_sem_semctl() H

�owx_inode_permission() H �owx_sem_alloc_security() H�owx_inode_mknod() H �owx_sem_free_security() H

�owx_inode_post_mknod() N Memoria Compartida SHM�owx_inode_rename() H �owx_shm_open() N

Capítulo 8 �owx_shm_post_create() N- - �owx_shm_shmctl() H

Capítulo 9 �owx_shm_alloc_security() H�owx_setgid() H �owx_shm_free_security() H�owx_setuid() H �owx_shm_shmat() HCapítulo 10 �owx_interrupt_hook() N

Colas de Mensajes POSIX Mapeos en Memoria�owx_posix_mqueue_open() N �owx_�le_mmap() H

�owx_posix_mqueue_post_send() N - -�owx_posix_mqueue_inode_time() N - -

Cuadro 11.1: Hooks utilizados por el prototipo Flowx. H = hook original de LSM. N = hooks agregadopor Flowx

Page 183: Flowx: Implementación de no interferencia en Linux

Capítulo 12

Trabajo Futuro

Futuras versiones de Flowx deberán tener en cuentas aspectos relativos a la seguridad que no fueronabarcados por el prototipo descrito en este trabajo. A lo largo del informe se han comentado algunosde los huecos a rellenar en el futuro, pero en este capítulo se cubrirán con un poco más de detalle.

12.1. Comunicación de Procesos en Red

Los programas en espacio de usuario poseen mecanismos para sincronizar sus acciones e intercam-biar información con otros programas residentes en difentes máquinas. Para esto usan un mecanismode conexión basado en sockets. Estos permiten que procesos en diferentes computadoras intercambieninformación a través de la red. Los sockets también pueden ser usados como una herramienta de co-municación para procesos residentes en la misma máquina. El sistema de ventanas X, por ejemplo, usaun socket para permitir que un programa cliente comparta datos con el servidor X. El modelo deberáintervenir tanto en conexiones socket internas como externas.

12.2. Sistema de Ventanas X

El sistema de ventanas X fue desarrollado a mediados de los 80 para dotar a los sistemas Unixde una interfaz grá�ca. Este protocolo permite la interacción grá�ca entre un usuario y una o máscomputadoras haciendo transparente la red para aquel. Generalmente se re�ere a la versión 11 de esteprotocolo, X11, el que está en uso actualmente.

X es el encargado de mostrar la información grá�ca y es totalmente independiente del sistemaoperativo. El sistema de ventanas X distribuye el procesamiento de aplicaciones especi�cando enlacescliente-servidor. El servidor provee servicios para acceder a la pantalla, teclado y ratón, mientras quelos clientes son las aplicaciones que utilizan estos recursos para interacción con el usuario. De estemodo mientras el servidor se ejecuta de manera local, las aplicaciones pueden ejecutarse remotamentedesde otras máquinas, proporcionando así el concepto de transparencia de red.

Algunas consideraciones que el módulo deberá tener con respecto al uso de este sistema son:

¾Cómo extender el requisito del input compartido, cuando los programas corran sobre aplicacionesgrá�cas?

¾Cómo extender o modi�car nuestra implementación de trusted path para poder lanzarlo mientrasse esté ejecutando algún gestor de ventanas especí�co?

181

Page 184: Flowx: Implementación de no interferencia en Linux

CAPÍTULO 12. TRABAJO FUTURO 182

12.3. Canales Encubiertos

En Linux se pueden encontrar numerosos canales encubiertos, principalmente, cuando se deja queprocesos de alto nivel puedan modi�car recursos a los que procesos de bajo nivel puedan tener acceso.Este trabajo imposibilita la trasmisión de información a través de muchos de estos canales, pero no secubre la totalidad de éstos. Para citar un ejemplo, no tendríamos que permitir que un proceso secretomodi�que libremente la hora del sistema.

En el futuro se deberá centrar en reconocer el resto de los canales que Flowx no trata y buscarsoluciones adecuadas para proteger al sistema contra estas amenazas.

Page 185: Flowx: Implementación de no interferencia en Linux

Bibliografía

[1] Gasser, M., �Building a Secure Computer System�, Van Nostrand Reinhold, New York, 1988.

[2] Abrams, M., H. Podell and S. Jajodia, �Infomation Security�An integrated Collection of Essays�,IEEE Computer Society Press, 1995.

[3] Bell, D. and L. LaPadula, �Secure Computer Systems: A Mathematical Model�, Vol. II, MITRETechnical Report 2547, The MITRE Corporation, Bedford, MA 1973/74.

[4] Goguen J. and Meseguer J., �Security Policies and Security Models�, Proceedings of the 1982 IEEESymp. on Security and Privacy, 1982.

[5] Denning, D., �A Lattice Model of Secure Information Flow�, Comunication of the ACM, 19(5):236-243, may 1976.

[6] Volpano, D., Irvine, C. and Smith G., �A Sound Type System for Secure Flow Analysis�, Journalof Computer Security, Vol. 4, No. 3, pp. 167-187, December 1996.

[7] Sabelfeld, A., Myers, A., �Language-Based Information Flow Security�, IEEE Journal on SelectedAreas in Communications, Vol. 21, No. 1, January 2003.

[8] Cristiá, M., �I/O, Pointers and Noninterference of Terminating Programs Running On a GeneralComputing System�, 2007.

[9] McCullough, D., �Speci�cations for multi-level security and hook-up property�, Proceedings of the1987 IEEE Symp. on Security and Privacy, 1987.

[10] Corbet, J., Rubini, A., Kroah-Hartman, G., �Linux Device Drivers, 3rd Edition�, O'Reilly, Febru-ary 2005.

[11] Wright, C., Cowan, C., Morris, J., Smalley, S. and Kroah-Hartman, G., �Linux Security Modules:General Security Support for the Linux Kernel�, Proceedings of the 11th USENIX Security Symp.,August 2002.

[12] Kroah-Hartman, G., �udev, a Userspace Implementation of devfs�, Proceedings of the Linux Symp.,July 2003.

[13] Mochel, P., �The sysfs Filesystem�, www.kernel.org/pub/linux/kernel/people/mochel/doc/-papers/ols-2005/mochel.pdf.

[14] Bovet, D. and M. Cesati, �Understanding the Linux Kernel, 3rd Edition�, O'Reilly, November 2005.

[15] Love, R., �Linux Kernel Development, 2nd Edition�, O'Reilly, January 2005.

[16] Tigran A., �Linux Kernel 2.4 Internals�, August 2002.

183

Page 186: Flowx: Implementación de no interferencia en Linux

BIBLIOGRAFÍA 184

[17] Steven, R., �Unix Network Programming: Interprocess Communications, 2nd Edition�, PrenticeHall, 1999.

[18] Archivo de la Linux Kernel Mailing List, http://lkml.org/.