stringtranslate.com

Tipo de seguridad

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

La aplicación de tipos puede ser estática, detectando posibles errores 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 ambas. [1] La aplicación de tipos dinámica a menudo permite ejecutar programas que serían inválidos con una 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: consulte, por ejemplo, subtipificación y polimorfismo para ver las complicaciones.

Definiciones

Intuitivamente, la solidez de los tipos queda reflejada en la concisa declaración de Robin Milner :

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

En otras palabras, si un sistema de tipos es sólido , 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 es seguro en cuanto a tipos si las únicas operaciones que se pueden realizar sobre los datos en el lenguaje son aquellas autorizadas por el tipo de los datos. [3]

Sin embargo, lo que significa exactamente 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 precisa y formal de la solidez de los tipos 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 y técnica de prueba estándar para la seguridad de tipos en lenguajes definidos por la semántica operacional [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 sólida en cuanto a tipos:

Progreso
Un programa bien tipificado nunca se queda "atascado": 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 son 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 permanece igual (es decir, su tipo se conserva ).

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

Relación con otras formas de seguridad

De manera aislada, la solidez de los tipos es una propiedad relativamente débil, ya que básicamente solo establece que las reglas de un sistema de tipos son consistentes internamente y no se pueden subvertir. Sin embargo, en la práctica, los lenguajes de programación están diseñados de modo que la buena tipificación también implique otras propiedades más sólidas, algunas de las cuales incluyen:

Lenguajes con seguridad de tipos y lenguajes con falta de seguridad de tipos

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 de 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 la comprobación de miles de casos. Sin embargo, se ha demostrado que algunos lenguajes como Standard ML , que tiene una semántica rigurosamente definida, cumplen con una definición de seguridad de tipos. [8] Se cree que otros lenguajes como Haskell [ discutir ] cumplen con alguna definición de seguridad de tipos, siempre que no se utilicen ciertas características de "escape" (por ejemplo, unsafePerformIO de Haskell , utilizado para "escapar" del entorno restringido habitual en el que es posible la E/S, elude el sistema de tipos y, por lo tanto, puede utilizarse para romper la seguridad de tipos. [9] ) El juego de palabras con tipos es otro ejemplo de dicha característica de "escape". Independientemente de las propiedades de la definición del lenguaje, pueden producirse ciertos errores en el 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 no sea seguro en determinadas circunstancias. Una versión anterior de la máquina virtual Java de Sun era vulnerable a este tipo de problema. [3]

Tipificación fuerte y débil

Los lenguajes de programación se clasifican coloquialmente como fuertemente tipados o débilmente tipados (también débilmente tipados) para referirse a ciertos aspectos de la seguridad de tipos. En 1974, Liskov y Zilles definieron un lenguaje fuertemente tipado como uno en el que "siempre que un objeto se 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 tipos implícita. [12]

Gestión de memoria y seguridad de tipos

La seguridad de tipos está estrechamente vinculada a la seguridad de 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 error de tipo cuando se lee la variable. Por el contrario, si el lenguaje es seguro para la memoria, no puede permitir que se use un entero arbitrario como puntero , por lo tanto, debe haber un puntero o tipo de referencia separado.

Como condición mínima, un lenguaje de tipo seguro no debe permitir punteros colgantes entre asignaciones de diferentes tipos. Pero la mayoría de los lenguajes exigen el uso adecuado de tipos de datos abstractos definidos por programadores incluso cuando esto no es estrictamente necesario para la seguridad de la memoria o para la prevención de cualquier tipo de fallo catastrófico. A las asignaciones se les asigna un tipo que describe su contenido, y este tipo es fijo 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 la 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 seguro en cuanto a tipos y utiliza un verificador de préstamos 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 , lo que se expresa en términos de definiciones de clases.

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

Cada función que intercambia objetos derivados de una clase específica, o que implementa una interfaz específica , se adherirá a ese contrato: por lo tanto, en esa función las operaciones permitidas sobre ese objeto serán solo aquellas definidas por los métodos de la clase que el objeto implementa. Esto garantizará que se preserve 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.

Problemas de seguridad de tipos en lenguajes específicos

Ada

Ada fue diseñado para ser adecuado para sistemas embebidos , controladores de dispositivos y otras formas de programación de sistemas , pero también para fomentar la programación de tipo seguro. Para resolver estos objetivos conflictivos, Ada limita la falta de seguridad de tipos a un determinado conjunto de construcciones especiales cuyos nombres suelen empezar con la cadena Unchecked_ . Unchecked_Deallocation se puede prohibir de forma eficaz en una unidad de texto de Ada aplicando pragma Pure a esta unidad. Se espera que los programadores utilicen las construcciones Unchecked_ con mucho cuidado y solo cuando sea necesario; los programas que no las utilizan son de tipo seguro.

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 con verificación estática a las características del lenguaje disponibles. SPARK evita los problemas con los punteros colgantes al prohibir por completo la asignación en tiempo de ejecución.

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

do

El lenguaje de programación C es seguro en cuanto a 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 son seguras en cuanto a tipos; por ejemplo, la forma habitual de imprimir un entero es algo como printf("%d", 12), donde %dindica printfen tiempo de ejecución que espere un argumento entero. (Algo como printf("%s", 12), que indica a la función que espere un puntero a una cadena de caracteres y, sin embargo, proporciona un argumento entero, puede ser aceptado por los compiladores, pero producirá resultados indefinidos). Esto se mitiga parcialmente con algunos compiladores (como gcc) que comprueban las correspondencias de tipos 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 inseguro en cuanto a tipos. 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 indica cuántos bytes se requieren. La función devuelve un puntero sin tipo (tipo void *), que el código que lo llama 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 tanto, 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 más seguro en términos de tipos:

DO#

C# es un sistema de seguridad de tipos. Tiene soporte para punteros sin tipo, pero se debe acceder a ellos mediante la palabra clave "unsafe", que se puede prohibir en el nivel del compilador. Tiene soporte inherente para la validación de conversiones en tiempo de ejecución. Las conversiones se pueden validar mediante la palabra clave "as", que devolverá una referencia nula si la conversión no es válida, o mediante una conversión al estilo C que lanzará una excepción si la conversión no es válida. Consulte Operadores de conversión de C Sharp .

La dependencia excesiva del tipo de objeto (del que se derivan todos los demás tipos) corre el riesgo de frustrar el propósito del sistema de tipos de C#. Por lo general, es mejor abandonar las referencias a objetos en favor de los genéricos , similares a las plantillas en C++ y los genéricos en Java .

Java

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

Para implementar la aplicación de la seguridad de tipos , cada objeto, antes de su uso, debe tener una asignación . Java permite el uso de tipos primitivos , pero solo 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 solo maneja números que se pueden expresar 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 una 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 lo tanto, la biblioteca, en lugar del lenguaje, garantiza que el objeto respete el contrato implícito en la definición de la clase.

ML estándar

El ML estándar tiene una semántica rigurosamente definida y se sabe que es seguro en cuanto a tipos. Sin embargo, algunas implementaciones, incluidas el ML estándar de Nueva Jersey (SML/NJ), su variante sintáctica Mythryl y MLton , proporcionan bibliotecas que ofrecen operaciones no seguras. 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 formas específicas. Otro ejemplo es el propio nivel superior interactivo de SML/NJ , que debe utilizar operaciones no seguras 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 exige que cualquier recurso inseguro se marque explícitamente como inseguro. Esto se logra "moviendo" dichos recursos a una pseudo-biblioteca incorporada llamada SYSTEM desde donde deben ser importados antes de que puedan ser utilizados. De esta manera, la importación hace visible el uso de dichos recursos. Desafortunadamente, esto no se implementó en consecuencia en el informe del lenguaje original y su implementación. [16] Aún quedaban recursos inseguros como la sintaxis de conversión de tipos y los registros de variantes (heredados de Pascal) que podían usarse sin una importación previa. [17] La ​​dificultad para mover estos recursos al pseudo-módulo SYSTEM era la falta de un identificador para el recurso que luego pudiera ser importado, ya que solo se pueden importar identificadores, pero no sintaxis.

IMPORTAR  SISTEMA ; (*  permite el uso de ciertas funciones no seguras: * ) VAR palabra  :  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  :  CARDINAL ; n  :=  CARDINAL ( i );  (* o *)  i  :=  INTEGER ( n );

La norma ISO Modula-2 corrigió este problema para la función de conversión de tipos al cambiar la sintaxis de conversión de tipos a una función llamada CAST que debe importarse desde el pseudomódulo SYSTEM. Sin embargo, otras funciones no seguras, 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  :  CARDINAL ; i  :=  SISTEMA . CONVERSIÓN ( ENTERO ,  n );  (* Conversión de tipo en ISO Modula-2 *)

Una revisión reciente del lenguaje aplicó rigurosamente la filosofía de diseño original. Primero, el pseudomódulo SYSTEM fue renombrado a UNSAFE para hacer más explícita la naturaleza insegura de las facilidades importadas desde allí. Luego, todas las facilidades inseguras restantes fueron eliminadas por completo (por ejemplo, registros de variantes) o movidas al pseudomódulo UNSAFE. Para las facilidades donde no hay un identificador que pueda ser importado, se introdujeron identificadores de habilitación. Para habilitar dicha facilidad, su identificador de habilitación correspondiente debe ser importado desde el pseudomódulo UNSAFE. No quedan facilidades inseguras en el lenguaje que no requieran importación desde UNSAFE. [17]

IMPORTAR  UNSAFE ; VAR  i  :  ENTERO ;  n  :  CARDINAL ; i  :=  UNSAFE . CAST ( ENTERO ,  n );  (* Conversión de tipos en Modula-2 Revisión 2010 *)DE  IMPORTACIÓN INSEGURA  FFI ; (* identificador de habilitación para la interfaz de función externa *) <*FFI="C"*> (* pragma para la 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 de Pascal dicta "tipificación estricta", no se pueden asignar dos variables entre sí a menos que sean compatibles (como la conversión de un entero a un real) o se asignen al mismo subtipo. Por ejemplo, si tiene el siguiente fragmento de código:

tipo TwoTypes = registro I : Entero ; Q : Real ; fin ;         DualTypes = registro I : entero ; Q : real ; fin ;       var T1 , T2 : DosTipos ; D1 , D2 : DosTipos ;      

En el caso de una 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 sean idénticos) y una asignación de es ilegal. Una asignación de sería legal porque los subtipos a los que están definidas son idénticos. Sin embargo, una asignación como 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 de Common Lisp es responsable de insertar comprobaciones dinámicas para operaciones cuya seguridad de tipos no se puede probar estáticamente. Sin embargo, un programador puede indicar que un programa se debe compilar con un nivel inferior de comprobación de tipos dinámica. [19] Un programa compilado en dicho modo no puede considerarse seguro en cuanto a tipos.

Ejemplos de C++

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

#include <iostream> usando el espacio de nombres std ;   int main () { int ival = 5 ; // valor entero float fval = reinterpret_cast < float &> ( ival ); // reinterpretar patrón de bits cout << fval << endl ; // generar entero como float return 0 ; }                     

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

El siguiente ejemplo muestra cómo las referencias a objetos pueden ser convertidas incorrectamente:

#include <iostream> usando el espacio de nombres std ;   clase Padre { público : virtual ~ Padre () {} // destructor virtual para RTTI };      clase Niño1 : público Padre { público : int a ; };       clase Child2 : público Padre { público : float b ; };       int main () { Child1 c1 ; c1 . a = 5 ; Parent & p = c1 ; // conversión ascendente siempre segura Child2 & c2 = static_cast < Child2 &> ( p ); // conversión descendente no válida cout << c2 . b << endl ; // generará datos basura return 0 ; }                            

Las dos clases secundarias tienen miembros de tipos diferentes. Al convertir un puntero de clase primaria a un puntero de clase secundaria, el puntero resultante puede no apuntar a un objeto válido del tipo correcto. En el ejemplo, esto hace que se imprima un valor basura. El problema se podría haber evitado reemplazando static_castpor dynamic_castque lanza una excepción en las conversiones no válidas. [21]

Véase también

Notas

  1. ^ "Qué hay que saber antes de debatir sobre sistemas de tipos | Ovid [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 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 los tipos" . Consultado el 8 de octubre de 2008 .
  4. ^ Wright, AK; Felleisen, M. (15 de noviembre de 1994). "Un enfoque sintáctico de la solidez de los tipos". 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). "Principal type-schemes for functional programs". Actas del 9º simposio ACM SIGPLAN-SIGACT sobre Principios de lenguajes de programación - POPL '82 . Association for Computing Machinery. págs. 207–212. doi :10.1145/582153.582176. ISBN 0897910656.S2CID11319320  .​
  6. ^ Tofte, Mads (1988). Semántica operacional e inferencia de tipos polimórficos (Tesis).
  7. ^ Henriksen, Troels; Elsman, Martin (17 de junio de 2021). "Hacia tipos dependientes del tamaño para la programación de matrices". Actas del 7.º Taller internacional ACM SIGPLAN sobre bibliotecas, lenguajes y compiladores para programación de matrices . Association for Computing Machinery. págs. 1–14. doi :10.1145/3460944.3464310. ISBN 9781450384667. Número de identificación del sujeto  235474098.
  8. ^ ML estándar. Smlnj.org. Recuperado el 2 de noviembre de 2013.
  9. ^ "System.IO.Unsafe". Manual de bibliotecas de GHC: base-3.0.1.0 . Archivado desde el original el 2008-07-05 . Consultado el 2008-07-17 .
  10. ^ Liskov, B; Zilles, S (1974). "Programación con tipos de datos abstractos". ACM SIGPLAN Notices . 9 (4): 50–59. CiteSeerX 10.1.1.136.3043 . doi :10.1145/942572.807045. 
  11. ^ Jackson, K. (1977). "Procesamiento paralelo y construcción modular de software". Diseño e implementación de lenguajes de programación . Apuntes de clase en 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 orientada a objetos. 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, Benjamin C. (2002). Tipos y lenguajes de programación . Cambridge, Mass.: MIT Press. p. 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, NJ : Prentice Hall . p. 116. ISBN 978-0-13-110362-7En C , el método adecuado es declarar que malloc devuelve un puntero a void y luego convertir 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. ^ "Common Lisp HyperSpec" . Consultado el 26 de mayo de 2013 .
  20. ^ "conversión reinterpret_cast - cppreference.com". En.cppreference.com . Consultado el 21 de septiembre de 2022 .
  21. ^ "conversión dynamic_cast - cppreference.com". En.cppreference.com . Consultado el 21 de septiembre de 2022 .

Referencias