Técnicas de Diseño de Software Crítico de Seguridad

03.01.2020

Presentamos en este artículo, un conjunto de estrategias que se utilizan cuando se desarrolla software crítico de seguridad, típico en aplicaciones ferroviarios con nivel SIL-4 o inferior o en equipamiento electrónico embarcado de seguridad. 

Estas aplicaciones además de cumplir con la normativa EN 50128 (y EN 50155 en aplicaciones electrónicas en trenes o aplicaciones embarcadas), deben estar desarrolladas con el objetivo de ser robustas frente a fallos internos y externos que durante todo su tiempo de vida en funcionamiento, sin duda, van a sufrir. 

La seguridad de un sistema electrónico con microcontrolador depende de la arquitectura del hardware pero también de su software y así lo define la normativa CENELEC EN50129 y EN50128. Para el software, la gestión de la seguridad puede realizarse mediante el uso de algunas técnicas que presentamos en este artículo. El objetivo de estas técnicas es poder gestionar estados incorrectos de las aplicaciones software, es decir, detectarlos y en algunas ocasiones, cuando es posible, volver al sistema al modo de funcionamiento correcto (ya sea con todas las prestaciones en funcionamiento o en modo degradado).

Cuando se trata de hacer que las aplicaciones de software sean seguras, se utilizan normalmente las siguientes técnicas de desarrollo software:

  • detección de errores
  • recuperación de errores
  • programación defensiva
  • doble ejecución de la aplicación de software
  • redundancia de datos

Presentaremos, por tanto, en este artículo, cada una de estas técnicas de forma detallada. Es importante tener en cuenta que la aplicación de estas técnicas implica añadir un gran número de líneas de código (puede considerarse como referencia las mismas que son necesarias para el cumplimiento de los requisitos funcionales iniciales), teniendo finalmente un diseño y un código fuente el doble de grande de lo proyectado para el sistema inicial. Por tanto, doblamos potencialmente la aparición de fallos y bugs en el código, pudiendo llegar a empeorar, paradójicamente, la seguridad al añadir estas estrategias. Por tanto, es importante cuidar el desarrollo de este software, aplicar los procesos de calidad que sean necesarios y validar/verificar con el esfuerzo que implica este nuevo escenario. Finalmente, racionalizar al máximo estas técnicas ya que su uso implica una complejidad que puede acabar penalizando en vez de aportar al performance general del sistema.

DETECCIÓN DE ERRORES

El objetivo de la detección de errores es no pasar por alto la aparición de un error en la ejecución del código y, por tanto, su misión principal es poder indicar que el código en ejecución se encuentra frente a un error o circunstancia excepcional. Típicamente, en circunstancias normales, el resultado de una operación (de una función, de un procedimiento o de un segmento de código), devuelve un resultado de cierto tipo acotado por la propia definición del tipo que devuelve. En cambio, cuando surgen circunstancias excepcionales, el código no puede devolver un resultado acotado al tipo debido al entorno erróneo que se está produciendo. La solución más utilizada es diseñar un código y una extensión de tipo mediante una constante especial "indefinida" que, frente a una anomalía, el sistema diseñado tenga la capacidad de devolver como resultado la constante "indefinida". Frente a esta solución, algunos programadores prefieren implantar retorno de tipos complejos, con la(s) variable(s) inicial(es) más una variable extra, por ejemplo binaria, que indica si la variable primaria que se devuelve se ha calculado en base a un procedimiento correcto. Otro mecanismo muy utilizado cuando se codifica, por ejemplo en C++, para el procesamiento de errores es el mecanismo de excepción. La gestión de excepciones proporciona una codificación clara y normalizada, pero deslocaliza el procesamiento de errores y, por tanto, aumenta su complejidad y trazabilidad. En todo caso, es vital codificar el software con el objetivo de detectar errores que pudieran aparecer durante su ejecución. 


RECUPERACIÓN DE ERRORES

La recuperación de errores de ejecución software tiene como objetivo volver a poner el sistema completo en un estado correcto después de la detección de un error. Típicamente se definen dos posibilidades en cuanto a la recuperación de errores:

  • Recuperación de errores mediante reintentos.

La recuperación "hacia atrás" implica tener y ejecutar un mecanismo dentro del software para restaurar el sistema a un estado seguro. Para hacerlo, el sistema debe autoguardarse regularmente y los sistemas guardados deben permitir su "recarga" (se entiende el sistema como aquellos parámetros, variables y estados del sistema en conjunto que cambian a lo largo del hilo de ejecución). Cuando se detecta una situación errónea, es posible volver a cargar una de las situaciones anteriores (en la mayoría de los casos la más reciente) y volver a iniciar la ejecución desde aquel punto. Si el error se origina en el entorno o en una falla transitoria, el sistema puede adoptar un modo operativo correcto a partir de ahí ya que la situación de falla, teóricamente al ser transitoria no volverá a suceder. Evidentemente, si el fallo es sistemático (de hardware o de software), el sistema volverá al modo de fallo ya que las mismas condiciones volverán a reproducirse. Frente a estos errores sistemáticos, algunos sistemas muy avanzados, pueden tener varias alternativas para las aplicaciones de software y activan una réplica diferente de la aplicación cuando detectan un error que se repite sistemáticamente.

El sistema más sencillo y típico utilizado en recuperación de errores mediante reintentos es la de ejecutar un reset al microcontrolador, volviéndolo a un estado 100% conocido y controlado. Aunque ya hemos visto que no tiene por qué ser la única estrategia que se utilice en un proceso de reintento. Si se utiliza un buen mecanismo de guardado de datos clave periódico y un sistema de recarga inteligente, no es necesario forzar un reset al hardware del sistema. Como puede imaginarse, una de las claves de estos diseños será una correcta definición de los puntos de ejecución donde estos autoguardados tienen lugar

  • Recuperación de errores a través de la continuación

La recuperación "hacia adelante" implica, frente a la detección de un error, seguir ejecutando la aplicación desde un estado erróneo, intentando hacer correcciones selectivas al estado del sistema. Es aquí cuando entrarían de nuevo, por ejemplo, las excepciones ya comentadas de C++.

La recuperación "hacia adelante" es una técnica que podemos considerar compleja de implementar, siendo compleja también su validación para comprobar su efectividad.

De alguna manera, la recuperación "hacia adelante" requiere una lista completa de errores y su estrategia de como corregirlos. Agregar puntos de detección de errores y puntos de verificación de operación aumentará la complejidad del código y aumentará las combinaciones de pruebas que se realizarán para validar la aplicación.  


PROGRAMACIÓN DEFENSIVA

La programación defensiva se basa en producir programas capaces de detectar flujos de comandos, datos o valores de datos erróneos durante la ejecución, y reaccionar a estos errores de una manera predeterminada y aceptable:

  • La primera norma de la programación defensiva, aunque obvia, imperativa, es utilizar la inicialización restrictiva de todas las variables (generales y locales). Además y muy importante, la elección de valores para la inicialización debe estar relacionada con la noción de estado seguro.
  • Checksum y verificación de memorias. Durante el arranque del sistema y periódicamente en fase de ejecución, se llevan a cabo testeos de que la memoria de programa y de datos no ha tenido modificaciones de algún tipo por algún efecto no provocado por el propio sistema. En este sentido también típico el "rellenado" de memoria que no se utiliza para asegurar que no se malinterpreta por alguna instrucción durante la fase de ejecución. La verificación de memorias tiene como objetivo asegurar que en todas las posiciones de memoria que utiliza el sistema, se puede leer y escribir correctamente. Esta operación también se lleva a cabo durante el arranque del sistema y periódicamente en fase de ejecución.
  • La segunda norma es administrar la coherencia de las entradas, para evitar que una entrada errónea se propague a través de la aplicación de software. De este modo, de nuevo es relevante verificar sistemáticamente las entradas (parámetros de una consulta de función, variables generales, retornos de consulta de función, etc.). Por ejemplo, si es imposible que una función tenga un valor negativo en su entrada, formará parte de un programación defensiva, comprobar que el valor de entrada no es negativo en ninguno de los casos, debiéndose siempre de aplicar las restricciones más acotadas posibles.
  • El tercer enfoque está relacionado con la gestión de datos durante el procesamiento. Para hacer esto, para cada estructura de programación, se debe tener en cuenta el contexto de cada uso diferente. Para una instrucción IF, sistemáticamente habrá una rama ELSE. Para una instrucción SWITCH, debe haber sistemáticamente un DEFAULT. Es decir, siempre hay un hilo programado y bajo control al que puede ir la ejecución del software aunque hubiera un fallo.
  • Plausibilidad de los datos ¿Los valores de las variables parecen plausibles, verosímiles, probables o tienen sentido, dado el conocimiento que tenemos del programa? Cuando se trata de la coherencia de los datos de entrada, se acostumbra a implantar acciones redundantes en tiempo, es decir, se captura dos o más veces el mismo valor con el objetivo de obtener el mismo resultado (o hacer un proceso de votación de n capturas) o se utiliza dos generadores de entrada distintos para misma fuente (dos sensores por ejemplo en paralelo, sensando la misma fuente, deben presentar el mismo valor). Para algunos sistemas, existen condiciones que ayudan a verificar la coherencia del dato de entrada, como por ejemplo un sensor de ángulo de una pieza giratoria: la medición debe pasar por todos los ángulos de grado en grado y no es posible hacer saltos de 90º grados. Tener un sistema con esta propiedad debe aprovecharse y no aceptar valores que generen, por ejemplo, saltos de ángulos que son físicamente imposibles.
  • Plausibilidad de la operación Las técnicas relacionadas con la plausibilidad de la operación giran en torno a la pregunta "¿La ejecución del programa sigue un flujo predecible y esperado?". La coherencia en la operación ayuda a evaluar si una aplicación de software dada sigue una ruta previamente validada. Para hacerlo, la ejecución de la aplicación de software debe ser rastreable mediante el marcaje de pasos que se han llevado a cabo. La ejecución se compone de una serie de trazas, cada una de las cuales es una serie de puntos de control (ruta de ejecución).
  • Watchdog. El watchdog es una de las técnicas más utilizadas para asegurar que un software está, como mínimo, lo suficientemente "vivo" como para refrescar periódicamente un flag que mantiene el watchdog sin resetear el microcontrolador.
  • Refresco de bits de configuración. Es muy habitual refrescar periódicamente los bits de configuración del microcontrolador que sin una orientación de programación defensiva, sólo se configuran una vez en el inicio del programa. Es más, para poder evaluar posibles problemas, se lee la configuración, se certifica que sigue teniendo el valor que toca y, se sobreescribe de nuevo el valor de configuración. En algunas ocasiones como en procesos de lectura y escritura de puertos de entrada y salida, se aprovecha en estos procesos para reconfigurar estos puertos.

DOBLE EJECUCIÓN DE LA APLICACIÓN DE SOFTWARE

La redundancia operativa utiliza dos veces la misma aplicación a través de la misma unidad de tratamiento (procesador, etc.). Los resultados generados en dos tiempos distintos se comparan a través de un dispositivo interno o externo al procesador y cualquier inconsistencia provoca una entrada a un modo degradado del sistema. Esta técnica de redundancia operativa puede ayudar a detectar, por ejemplo, fallos de memoria. Un programa único se carga en dos zonas diferentes del medio de almacenamiento (dos zonas diferentes en la memoria de direccionamiento, dos soportes de memoria diferentes, etc.). Los fallos de memoria (RAM, ROM, EPROM, etc.) se pueden detectar junto con fallos aleatorios de las unidades de procesamiento. Cabe señalar que algunos fallos de los dispositivos de hardware compartidos (unidad de comparación, unidad de tratamiento) no se detectan y, por lo tanto, permanecen latentes.

Una ampliación del nivel de redundancia consiste en introducir la diversificación del código. Esta diversificación puede ser "ligera", imponiendo el uso de dos conjuntos diferentes de instrucciones para programar la aplicación. Por ejemplo, uno de los programas usaría A + B, mientras que el otro usaría - (- A-B). Por otro lado puede hablarse de diversificación "compleja", utilizándose la redundancia completa, donde toda la aplicación es diseñada y ejecutada de forma independiente una de la otra.

La diversificación de software puede complementarse con la diversificación de hardware. Es decir, el mismo software o diferente software diversificado de forma "ligera" o "compleja" se ejecuta a la vez, en dos unidades hardware. El resultado de cada unidad hardware se intercambia hacia la otra para valorar la consistencia de los resultados o, se implanta una tercera unidad que evaluará que ambos sistema llegan a los mismos resultados. A este tipo de configuración se le llama 2 de 2 (2 out of 2, 2oo2). También es muy a frecuento, para aumentar la disponibilidad de equipos, utilizar estrategias 2 de 3 (2 out of 3, 2oo3), donde se establecerá un sistema de votación ejecutándose una acción mientras al menos 2 de los 3 sub-sistemas hardware, aporten el mismo resultado.  


REDUNDANCIA DE DATOS

En su forma más simple, la redundancia de datos duplica las variables almacenadas y compara su contenido para asegurar que se han inicializado correctamente, se han actualizado correctamente, se han leído correctamente y, durante el tiempo que han estado almacenadas en alguna memoria, no han sido cambiadas involuntariamente. También se entiende por redundancia de datos y esta vez, con un nivel mayor de complejidad, el uso de técnicas de integridad de datos con redundancia. Son ampliamente conocidos los, sistemas CRC, código Hamming, etc. La particularidad de estos últimos por ejemplo es su capacidad de corrección del error.


En Leedeo Engineering, somos especialistas en el desarrollo de proyectos de RAMS Ferroviarios donde el software critico y el cumplimiento de la normativa CENELEC EN 50128 es el eje vertebrador del proyecto, dando soporte a cualquier nivel requerido a las tareas RAM y de Safety, y tanto a nivel de infraestructura o equipamiento embarcado.

¿Te interesan nuestros artículos sobre Ingeniería RAMS y Tecnología?

Inscríbete en nuestra newsletter y te mantendremos informado de la publicación de nuevos artículos.