stringtranslate.com

Tipo de seguridad

En informática , la seguridad y solidez de los tipos son el grado en que un lenguaje de programación desalienta o previene los errores de tipo . A veces, alternativamente, se considera que la seguridad de tipos es una propiedad de las funciones de un lenguaje informático; es decir, algunas funciones son seguras para los tipos y su uso no dará lugar a errores tipográficos, mientras que otras funciones en el mismo lenguaje pueden no ser seguras para los tipos y un programa que las utilice puede encontrar errores tipográficos. Los comportamientos clasificados como errores de tipo por un lenguaje de programación determinado suelen ser aquellos que resultan de intentos de realizar operaciones sobre valores que no son del tipo de datos apropiado , por ejemplo, agregar una cadena a un número entero cuando no hay una definición sobre cómo manejar este caso. . Esta clasificación se basa en parte en opiniones.

La aplicación de tipos puede ser estática, detectando errores potenciales en tiempo de compilación , o dinámica, asociando información de tipos con valores en tiempo de ejecución y consultándolos según sea necesario para detectar errores inminentes, o una combinación de ambos. [1] La aplicación de tipos dinámicos a menudo permite que se ejecuten programas que no serían válidos bajo la aplicación estática.

En el contexto de los sistemas de tipos estáticos (en tiempo de compilación), la seguridad de tipos generalmente implica (entre otras cosas) una garantía de que el valor final de cualquier expresión será un miembro legítimo del tipo estático de esa expresión. El requisito preciso es más sutil que esto; véase, por ejemplo, subtipificación y polimorfismo para complicaciones.

Definiciones

Intuitivamente, la solidez tipográfica se capta en la concisa afirmación de Robin Milner de que

Los programas bien escritos no pueden "salir mal". [2]

En otras palabras, si un sistema de tipos es sólido , entonces las expresiones aceptadas por ese sistema de tipos deben evaluarse como un valor del tipo apropiado (en lugar de producir un valor de algún otro tipo no relacionado o fallar con un error de tipo). Vijay Saraswat proporciona la siguiente definición relacionada:

Un lenguaje tiene seguridad de tipos si las únicas operaciones que se pueden realizar con los datos en el lenguaje son las autorizadas por el tipo de datos. [3]

Sin embargo, lo que significa precisamente que un programa esté "bien tipificado" o "salga mal" son propiedades de su semántica estática y dinámica , que son específicas de cada lenguaje de programación. En consecuencia, una definición formal y precisa de la solidez del tipo depende del estilo de la semántica formal utilizada para especificar un lenguaje. En 1994, Andrew Wright y Matthias Felleisen formularon lo que se ha convertido en la definición estándar y la técnica de prueba para la seguridad de tipos en lenguajes definidos por la semántica operativa , [4] que es la más cercana a la noción de seguridad de tipos tal como la entienden la mayoría de los programadores. Según este enfoque, la semántica de un lenguaje debe tener las dos propiedades siguientes para ser considerada tipo-sonido:

Progreso
Un programa bien escrito nunca se "atasa": cada expresión ya es un valor o puede reducirse a un valor de alguna manera bien definida. En otras palabras, el programa nunca llega a un estado indefinido en el que no sean posibles más transiciones.
Preservación (o reducción de sujeto )
Después de cada paso de evaluación, el tipo de cada expresión sigue siendo el mismo (es decir, se conserva su tipo ).

También se han publicado otros tratamientos formales de la solidez del tipo en términos de semántica denotacional y semántica operativa estructural . [2] [5] [6]

Relación con otras formas de seguridad

De forma aislada, la solidez de los tipos es una propiedad relativamente débil, ya que esencialmente simplemente establece que las reglas de un sistema de tipos son internamente consistentes y no pueden subvertirse. Sin embargo, en la práctica, los lenguajes de programación están diseñados para que la tipificación correcta también implique otras propiedades más sólidas, algunas de las cuales incluyen:

Lenguajes de tipo seguro y no seguro

La seguridad de tipos suele ser un requisito para cualquier lenguaje de juguete (es decir, lenguaje esotérico ) propuesto en la investigación académica sobre lenguajes de programación. Muchos lenguajes, por otro lado, son demasiado grandes para las pruebas de seguridad de tipos generadas por humanos, ya que a menudo requieren verificar miles de casos. Sin embargo, se ha demostrado que algunos lenguajes como Standard ML , que tiene una semántica definida rigurosamente, cumplen con una definición de seguridad de tipos. [8] Se cree [ discutir ] que algunos otros lenguajes como Haskell cumplen con alguna definición de seguridad de tipos, siempre que no se utilicen ciertas características de "escape" (por ejemplo, unsafePerformIO de Haskell , usado para "escapar" del entorno restringido habitual en el que uso). /O es posible, elude el sistema de tipos y, por lo tanto, puede usarse para romper la seguridad de tipos [9] ) Los juegos de palabras con tipos son otro ejemplo de esta característica de "escape". Independientemente de las propiedades de la definición del lenguaje, pueden ocurrir ciertos errores en tiempo de ejecución debido a errores en la implementación o en bibliotecas vinculadas escritas en otros lenguajes; Dichos errores podrían hacer que un tipo de implementación determinado sea inseguro en determinadas circunstancias. Una versión anterior de la máquina virtual Java de Sun era vulnerable a este tipo de problema. [3]

Escritura fuerte y débil

Los lenguajes de programación a menudo se clasifican coloquialmente como fuertemente tipados o débilmente tipados (también vagamente tipificados) para referirse a ciertos aspectos de la seguridad de tipos. En 1974, Liskov y Zilles definieron un lenguaje fuertemente tipado como aquel en el que "cada vez que un objeto pasa de una función que llama a una función llamada, su tipo debe ser compatible con el tipo declarado en la función llamada". [10] En 1977, Jackson escribió: "En un lenguaje fuertemente tipado, cada área de datos tendrá un tipo distinto y cada proceso indicará sus requisitos de comunicación en términos de estos tipos". [11] Por el contrario, un lenguaje débilmente tipado puede producir resultados impredecibles o puede realizar una conversión de tipo implícita. [12]

Gestión de memoria y seguridad de tipos.

La seguridad de tipos está estrechamente relacionada con la seguridad de la memoria . Por ejemplo, en una implementación de un lenguaje que tiene algún tipo que permite algunos patrones de bits pero no otros, un error de memoria de puntero colgante permite escribir un patrón de bits que no representa un miembro legítimo de en una variable inactiva de tipo , lo que provoca un tipo error al leer la variable. Por el contrario, si el lenguaje es seguro para la memoria, no puede permitir que se utilice un número entero arbitrario como puntero , por lo que debe haber un puntero o tipo de referencia independiente.

Como condición mínima, un lenguaje con seguridad de tipos no debe permitir punteros colgantes entre asignaciones de diferentes tipos. Pero la mayoría de los lenguajes imponen el uso adecuado de tipos de datos abstractos definidos por los programadores incluso cuando esto no es estrictamente necesario para la seguridad de la memoria o para la prevención de cualquier tipo de falla catastrófica. A las asignaciones se les asigna un tipo que describe su contenido, y este tipo se fija durante la duración de la asignación. Esto permite que el análisis de alias basado en tipos infiera que las asignaciones de diferentes tipos son distintas.

La mayoría de los lenguajes con seguridad de tipos utilizan la recolección de basura . Pierce dice que "es extremadamente difícil lograr seguridad de tipos en presencia de una operación de desasignación explícita", debido al problema del puntero colgante. [13] Sin embargo, Rust generalmente se considera de tipo seguro y utiliza un verificador de préstamo para lograr la seguridad de la memoria, en lugar de la recolección de basura.

Seguridad de tipos en lenguajes orientados a objetos

En los lenguajes orientados a objetos la seguridad de tipos suele ser intrínseca al hecho de que exista un sistema de tipos . Esto se expresa en términos de definiciones de clases.

Una clase define esencialmente la estructura de los objetos derivados de ella y una API como un contrato para manejar estos objetos. Cada vez que se crea un nuevo objeto cumplirá con ese contrato.

Cada función que intercambie objetos derivados de una clase específica, o que implemente una interfaz específica , se adherirá a ese contrato: por lo tanto, en esa función las operaciones permitidas sobre ese objeto serán sólo aquellas definidas por los métodos de la clase que implementa el objeto. Esto garantizará que se preservará la integridad del objeto. [14]

Las excepciones a esto son los lenguajes orientados a objetos que permiten la modificación dinámica de la estructura del objeto, o el uso de la reflexión para modificar el contenido de un objeto para superar las restricciones impuestas por las definiciones de los métodos de clase.

Escriba problemas de seguridad en idiomas específicos

ada

Ada fue diseñado para ser adecuado para sistemas integrados , controladores de dispositivos y otras formas de programación de sistemas , pero también para fomentar la programación con seguridad de tipos. Para resolver estos objetivos conflictivos, Ada limita la inseguridad de tipos a un determinado conjunto de construcciones especiales cuyos nombres generalmente comienzan con la cadena Unchecked_ . Unchecked_Deallocation se puede prohibir efectivamente en una unidad de texto de Ada aplicando pragma Pure a esta unidad. Se espera que los programadores utilicen construcciones Unchecked_ con mucho cuidado y sólo cuando sea necesario; los programas que no los utilizan tienen seguridad de tipos.

El lenguaje de programación SPARK es un subconjunto de Ada que elimina todas sus posibles ambigüedades e inseguridades y, al mismo tiempo, agrega contratos verificados estáticamente a las funciones del lenguaje disponibles. SPARK evita los problemas con los punteros pendientes al no permitir por completo la asignación en tiempo de ejecución.

Ada2012 agrega contratos verificados estáticamente al lenguaje mismo (en forma de condiciones previas y posteriores, así como invariantes de tipo).

C

El lenguaje de programación C tiene seguridad de tipos en contextos limitados; por ejemplo, se genera un error en tiempo de compilación cuando se intenta convertir un puntero a un tipo de estructura en un puntero a otro tipo de estructura, a menos que se utilice una conversión explícita. Sin embargo, varias operaciones muy comunes no tienen seguridad de tipos; por ejemplo, la forma habitual de imprimir un número entero es algo así como printf("%d", 12), donde %dindica printfen tiempo de ejecución que se espere un argumento de número entero. (Algo como printf("%s", 12), que le dice a la función que espere un puntero a una cadena de caracteres y aún así proporciona un argumento entero, puede ser aceptado por los compiladores, pero producirá resultados indefinidos). Esto se mitiga parcialmente mediante la verificación de algunos compiladores (como gcc) escriba correspondencias entre los argumentos de printf y las cadenas de formato.

Además, C, como Ada, proporciona conversiones explícitas no especificadas o no definidas; y a diferencia de Ada, los modismos que utilizan estas conversiones son muy comunes y han ayudado a darle a C una reputación de tipo inseguro. Por ejemplo, la forma estándar de asignar memoria en el montón es invocar una función de asignación de memoria, como malloc, con un argumento que indique cuántos bytes se requieren. La función devuelve un puntero sin tipo (tipo void *), que el código de llamada debe convertir explícita o implícitamente al tipo de puntero apropiado. Las implementaciones preestandarizadas de C requerían una conversión explícita para hacerlo, por lo que el código se convirtió en la práctica aceptada. [15](struct foo *) malloc(sizeof(struct foo))

C++

Algunas características de C++ que promueven un código con mayor seguridad de tipos:

C#

C# tiene seguridad de tipos. Tiene soporte para punteros sin tipo, pero se debe acceder a ellos utilizando la palabra clave "inseguro" que puede estar prohibida en el nivel del compilador. Tiene soporte inherente para la validación de transmisión en tiempo de ejecución. Las conversiones se pueden validar usando la palabra clave "as" que devolverá una referencia nula si la conversión no es válida, o usando una conversión estilo C que generará una excepción si la conversión no es válida. Consulte Operadores de conversión de C sostenido .

La confianza indebida en el tipo de objeto (del cual se derivan todos los demás tipos) corre el riesgo de frustrar el propósito del sistema de tipos de C#. Generalmente es una mejor práctica abandonar las referencias a objetos en favor de las genéricas , similares a las plantillas en C++ y las genéricas en Java .

Java

El lenguaje Java está diseñado para imponer la seguridad de tipos. Todo en Java sucede dentro de un objeto y cada objeto es una instancia de una clase .

Para implementar la aplicación de seguridad de tipo , cada objeto, antes de su uso, debe asignarse . Java permite el uso de tipos primitivos pero sólo dentro de objetos asignados correctamente.

A veces una parte de la seguridad de tipos se implementa indirectamente: por ejemplo, la clase BigDecimal representa un número de punto flotante de precisión arbitraria, pero sólo maneja números que pueden expresarse con una representación finita. La operación BigDecimal.divide() calcula un nuevo objeto como la división de dos números expresados ​​como BigDecimal.

En este caso, si la división no tiene representación finita, como cuando se calcula, por ejemplo, 1/3=0,33333..., el método divide() puede generar una excepción si no se define ningún modo de redondeo para la operación. Por tanto, la biblioteca, más que el lenguaje, garantiza que el objeto respete el contrato implícito en la definición de clase.

AA estándar

El ML estándar tiene una semántica rigurosamente definida y se sabe que tiene seguridad de tipos. Sin embargo, algunas implementaciones, incluido Standard ML of New Jersey (SML/NJ), su variante sintáctica Mythryl y MLton , proporcionan bibliotecas que ofrecen operaciones inseguras. Estas funciones se utilizan a menudo junto con las interfaces de funciones externas de esas implementaciones para interactuar con código que no es ML (como bibliotecas C) que pueden requerir datos dispuestos de maneras específicas. Otro ejemplo es el nivel superior interactivo SML/NJ , que debe utilizar operaciones inseguras para ejecutar el código ML ingresado por el usuario.

Módulo-2

Modula-2 es un lenguaje fuertemente tipado con una filosofía de diseño que requiere que cualquier instalación insegura se marque explícitamente como insegura. Esto se logra "trasladando" dichas instalaciones a una pseudobiblioteca integrada llamada SISTEMA desde donde se deben importar antes de poder utilizarlas. De este modo, la importación hace visible el momento en que se utilizan dichas instalaciones. Lamentablemente, esto no se implementó en el informe en el idioma original ni en su implementación. [16] Todavía quedaban instalaciones inseguras, como la sintaxis de conversión de tipos y registros variantes (heredados de Pascal) que podían usarse sin importación previa. [17] La ​​dificultad para mover estas funciones al pseudomódulo SYSTEM fue la falta de cualquier identificador para la instalación que luego pudiera importarse, ya que solo se pueden importar identificadores, pero no la sintaxis.

 SISTEMA DE IMPORTACIÓN ;  (* permite el uso de ciertas instalaciones no seguras: *) Palabra VAR  : SISTEMA . PALABRA ; dirección : SISTEMA . DIRECCIÓN ; dirección := SISTEMA . ADR ( palabra );       (* pero la sintaxis de conversión de tipos se puede utilizar sin dicha importación *) VAR  i  :  INTEGER ;  n  :  CARDENAL ; n  :=  CARDENAL ( i );  (* o *)  i  :=  ENTERO ( n );

El estándar ISO Modula-2 corrigió esto para la función de conversión de tipos cambiando la sintaxis de conversión de tipos a una función llamada CAST que debe importarse desde el pseudomódulo SISTEMA. Sin embargo, otras instalaciones inseguras, como los registros de variantes, permanecieron disponibles sin ninguna importación desde el pseudomódulo SYSTEM. [18]

 SISTEMA DE IMPORTACIÓN ; VAR  i  :  ENTERO ;  n  :  CARDENAL ; yo  :=  SISTEMA . CAST ( ENTERO ,  n );  (*Tipo fundido en ISO Modula-2*)

Una revisión reciente del lenguaje aplicó rigurosamente la filosofía de diseño original. En primer lugar, se cambió el nombre del pseudomódulo SISTEMA a INSEGURO para hacer más explícita la naturaleza insegura de las instalaciones importadas desde allí. Luego, todas las instalaciones inseguras restantes se eliminaron por completo (por ejemplo, registros de variantes) o se trasladaron al pseudomódulo INSEGURO. Para las instalaciones donde no existe ningún identificador que pueda importarse, se introdujeron identificadores habilitantes. Para habilitar dicha función, su correspondiente identificador de habilitación debe importarse del pseudomódulo UNSAFE. No quedan instalaciones inseguras en el idioma que no requieran importación de UNSAFE. [17]

IMPORTAR  INSEGURO ; VAR  i  :  ENTERO ;  n  :  CARDENAL ; i  :=  INSEGURO . CAST ( ENTERO ,  n );  (*Escrito en Modula-2 Revisión 2010*)DE  FFI DE IMPORTACIÓN PELIGROSA  ; (* identificador de habilitación para la función de interfaz de función externa *) <*FFI="C"*> (* pragma para interfaz de función externa a C *)   

Pascal

Pascal ha tenido una serie de requisitos de seguridad de tipos, algunos de los cuales se mantienen en algunos compiladores. Cuando un compilador Pascal dicta "tipificación estricta", dos variables no se pueden asignar entre sí a menos que sean compatibles (como la conversión de un número entero a real) o que estén asignadas al subtipo idéntico. Por ejemplo, si tiene el siguiente fragmento de código:

tipo TwoTypes = registro I : Entero ; P : Real ; fin ;         Tipos duales = registro I : entero ; P : Real ; fin ;       var T1 , T2 : dos tipos ; D1 , D2 : tipos duales ;      

Bajo tipificación estricta, una variable definida como TwoTypes no es compatible con DualTypes (porque no son idénticas, aunque los componentes de ese tipo definido por el usuario son idénticos) y una asignación de es ilegal. Una asignación de sería legal porque los subtipos a los que están definidos son idénticos. Sin embargo, una cesión como esta sería legal.T1 := D2;T1 := T2;T1.Q := D1.Q;

ceceo común

En general, Common Lisp es un lenguaje con seguridad de tipos. Un compilador Common Lisp es responsable de insertar comprobaciones dinámicas para operaciones cuya seguridad de tipo no se puede probar estáticamente. Sin embargo, un programador puede indicar que un programa debe compilarse con un nivel más bajo de verificación dinámica de tipos. [19] Un programa compilado en tal modo no puede considerarse de tipo seguro.

Ejemplos de C++

Los siguientes ejemplos ilustran cómo los operadores de conversión de C++ pueden romper la seguridad de tipos cuando se usan incorrectamente. El primer ejemplo muestra cómo los tipos de datos básicos se pueden convertir incorrectamente:

#include <iostream> usando el espacio de nombres std ;   int principal () { int ival = 5 ; // valor entero float fval = reinterpret_cast < float &> ( ival ); // reinterpreta el patrón de bits cout << fval << endl ; // genera un entero como valor flotante devuelve 0 ; }                     

En este ejemplo, reinterpret_castimpide explícitamente que el compilador realice una conversión segura de un valor entero a un valor de punto flotante. [20] Cuando el programa se ejecuta, generará un valor de punto flotante basura. El problema podría haberse evitado escribiendo en su lugarfloat fval = ival;

El siguiente ejemplo muestra cómo las referencias a objetos se pueden abatir incorrectamente:

#include <iostream> usando el espacio de nombres std ;   clase Padre { público : virtual ~ Padre () {} // destructor virtual para RTTI };      clase Niño1 : padre público { público : int a ; };       clase Niño2 : padre público { público : flotador b ; };       int principal () { Niño1 c1 ; c1 . a = 5 ; Padre & p = c1 ; // upcast siempre seguro Child2 & c2 = static_cast < Child2 &> ( p ); // cout abatido no válido << c2 . b << endl ; // generará datos basura return 0 ; }                            

Las dos clases secundarias tienen miembros de diferentes tipos. Al convertir un puntero de clase principal a un puntero de clase secundaria, es posible que el puntero resultante no apunte a un objeto válido del tipo correcto. En el ejemplo, esto lleva a que se imprima un valor basura. El problema podría haberse evitado reemplazándolo static_castcon dynamic_casteso arroja una excepción en conversiones no válidas. [21]

Ver también

Notas

  1. ^ "Qué saber antes de debatir sobre sistemas tipográficos | Ovidio [blogs.perl.org]". blogs.perl.org . Consultado el 27 de junio de 2023 .
  2. ^ ab Milner, Robin (1978), "Una teoría del polimorfismo de tipos en la programación", Journal of Computer and System Sciences , 17 (3): 348–375, doi : 10.1016/0022-0000(78)90014-4 , hdl : 20.500.11820/d16745d7-f113-44f0-a7a3-687c2b709f66
  3. ^ ab Saraswat, Vijay (15 de agosto de 1997). "Java no es seguro para escribir" . Consultado el 8 de octubre de 2008 .
  4. ^ Wright, Alaska; Felleisen, M. (15 de noviembre de 1994). "Un enfoque sintáctico para escribir solidez". Información y Computación . 115 (1): 38–94. doi : 10.1006/inco.1994.1093 . ISSN  0890-5401.
  5. ^ Damas, Luis; Milner, Robin (25 de enero de 1982). "Principales esquemas tipográficos para programas funcionales". Actas del noveno simposio ACM SIGPLAN-SIGACT sobre principios de lenguajes de programación - POPL '82 . Asociación para Maquinaria de Computación. págs. 207–212. doi :10.1145/582153.582176. ISBN 0897910656. S2CID  11319320.
  6. ^ Tofte, Mads (1988). Semántica operativa e inferencia de tipos polimórficos (Tesis).
  7. ^ Henriksen, Troels; Elsman, Martín (17 de junio de 2021). "Hacia tipos dependientes del tamaño para la programación de matrices". Actas del séptimo taller internacional ACM SIGPLAN sobre bibliotecas, lenguajes y compiladores para programación de matrices . Asociación para Maquinaria de Computación. págs. 1-14. doi :10.1145/3460944.3464310. ISBN 9781450384667. S2CID  235474098.
  8. ^ ML estándar. Smlnj.org. Recuperado el 2 de noviembre de 2013.
  9. ^ "System.IO.Inseguro". Manual de bibliotecas GHC: base-3.0.1.0 . Archivado desde el original el 5 de julio de 2008 . Consultado el 17 de julio de 2008 .
  10. ^ Liskov, B; Zilles, S (1974). "Programación con tipos de datos abstractos". Avisos ACM SIGPLAN . 9 (4): 50–59. CiteSeerX 10.1.1.136.3043 . doi :10.1145/942572.807045. 
  11. ^ Jackson, K. (1977). "Procesamiento paralelo y construcción de software modular". Diseño e Implementación de Lenguajes de Programación . Apuntes de conferencias sobre informática. vol. 54, págs. 436–443. doi :10.1007/BFb0021435. ISBN 3-540-08360-X.
  12. ^ "CS1130. Transición a la programación OO. - Primavera de 2012: versión a su propio ritmo". Universidad de Cornell, Departamento de Ciencias de la Computación. 2005 . Consultado el 15 de septiembre de 2023 .
  13. ^ Pierce, Benjamín C. (2002). Tipos y lenguajes de programación . Cambridge, Massachusetts: MIT Press. pag. 158.ISBN 0-262-16209-1.
  14. ^ La seguridad de tipos es, por tanto, también una cuestión de buena definición de clase: los métodos públicos que modifican el estado interno de un objeto deben preservar la integridad del objeto.
  15. ^ Kernighan ; Dennis M. Ritchie (marzo de 1988). El lenguaje de programación C (2ª ed.). Englewood Cliffs, Nueva Jersey : Prentice Hall . pag. 116.ISBN 978-0-13-110362-7. En C, el método adecuado es declarar que malloc devuelve un puntero a void y luego forzar explícitamente el puntero al tipo deseado con una conversión.
  16. ^ Niklaus Wirth (1985). Programación en Modula-2 . Springer Verlag.
  17. ^ ab "La separación de instalaciones seguras e inseguras" . Consultado el 24 de marzo de 2015 .
  18. ^ "Referencia del lenguaje ISO Modula-2" . Consultado el 24 de marzo de 2015 .
  19. ^ "HyperSpec de Lisp común" . Consultado el 26 de mayo de 2013 .
  20. ^ "reinterpret_cast conversion - cppreference.com". En.cppreference.com. Retrieved 2022-09-21.
  21. ^ "dynamic_cast conversion - cppreference.com". En.cppreference.com. Retrieved 2022-09-21.

References