Los lenguajes de programación seguros (PL) están ganando popularidad. Me pregunto cuál es la definición formal de PL seguro. Por ejemplo, C no es seguro, pero Java es seguro. Sospecho que la propiedad "segura" debería aplicarse a una implementación de PL en lugar de a la propia PL. Si es así, analicemos una definición de implementación segura de PL. Mis propios intentos de formalizar esta noción condujeron a un resultado extraño, por lo que me gustaría escuchar otras opiniones. Por favor, no diga que cada PL tiene comandos inseguros. Siempre podemos tomar un subconjunto seguro.
programming-languages
feroz
fuente
fuente
Respuestas:
Cuando llamamos a un idioma "seguro" en algún aspecto , eso significa formalmente que hay una prueba de que ningún programa bien formado en el idioma puede hacer algo que consideramos peligroso. La palabra "seguro" también se usa menos formalmente, pero eso es lo que la gente aquí entiende que significa su pregunta. Hay muchas definiciones diferentes de propiedades que queremos que tenga un lenguaje "seguro".
Algunos importantes son:
La definición de Andrew Wright y Matthias Felleisen de "solidez de tipo" , que se cita en muchos lugares (incluida Wikipedia) como una definición aceptada de "seguridad de tipo", y su prueba de 1994 de que un subconjunto de ML lo cumple.
Michael Hicks enumera varias definiciones de "seguridad de la memoria" aquí . Algunas son listas de tipos de errores que no pueden ocurrir, y otras se basan en tratar los punteros como capacidades. Java garantiza que ninguno de esos errores es posible (a menos que use explícitamente una característica marcada
unsafe
) haciendo que un recolector de basura administre todas las asignaciones y desasignaciones. Rust hace la misma garantía (de nuevo, a menos que marque explícitamente el código comounsafe
), a través de su sistema de tipo afín, que requiere que una variable sea propiedad o prestada antes de usarse como máximo una vez.Del mismo modo, el código seguro para subprocesos generalmente se define como un código que no puede exhibir ciertos tipos de errores que involucran subprocesos y memoria compartida, incluyendo carreras de datos y puntos muertos. Estas propiedades a menudo se aplican a nivel de lenguaje: Rust garantiza que las carreras de datos no pueden ocurrir en su sistema de tipos, C ++ garantiza que sus
std::shared_ptr
punteros inteligentes a los mismos objetos en múltiples hilos no eliminarán un objeto prematuramente o no podrán eliminarlo cuando la última referencia si se destruye, C y C ++ también tienenatomic
variables integradas en el lenguaje, con operaciones atómicas garantizadas para aplicar ciertos tipos de consistencia de memoria si se usan correctamente. MPI restringe la comunicación entre procesos a mensajes explícitos, y OpenMP tiene sintaxis para garantizar que el acceso a las variables de diferentes subprocesos sea seguro.La propiedad de que la memoria nunca se perderá a menudo se llama seguro por espacio. La recolección automática de basura es una característica de un idioma para garantizar esto.
Muchos idiomas tienen la garantía de que sus operaciones tendrán resultados bien definidos y que sus programas se comportarán bien. Como supercat dio un ejemplo de lo anterior, C hace esto para la aritmética sin signo (garantizado para envolver de forma segura) pero no para la aritmética con signo (donde se permite que el desbordamiento cause errores arbitrarios, porque C necesitaba admitir CPU que hacen cosas muy diferentes cuando la aritmética con signo desborda), pero luego el lenguaje a veces convierte silenciosamente cantidades sin signo en cantidades con signo.
Los lenguajes funcionales tienen una gran cantidad de invariantes que cualquier programa bien formado garantiza, por ejemplo, que las funciones puras no pueden causar efectos secundarios. Estos pueden o no ser descritos como "seguros".
Algunos idiomas, como SPARK u OCaml, están diseñados para facilitar la corrección del programa de prueba. Esto puede o no describirse como "seguro" contra errores.
Pruebas de que un sistema no puede violar un modelo de seguridad formal (de ahí el comentario, "Cualquier sistema que sea probablemente seguro probablemente no lo sea")
fuente
No existe una definición formal de "lenguaje de programación seguro"; Es una noción informal. Por el contrario, los idiomas que afirman proporcionar seguridad generalmente proporcionan una declaración formal precisa de qué tipo de seguridad se reclama / garantiza / proporciona. Por ejemplo, el lenguaje puede proporcionar seguridad de tipo, seguridad de memoria o alguna otra garantía similar.
fuente
double
olong
mientras está siendo modificado en otro hilo, que no está no garantizado para producir medio de un valor mezclado de algún modo no especificado con un medio de otro) pero la especificación API Sin embargo, tiene un comportamiento indefinido en algunos casos.Si puede obtener una copia de los Tipos y lenguajes de programación de Benjamin Pierce , en la introducción, tiene una buena visión general de varias perspectivas sobre el término "lenguaje seguro".
Una interpretación propuesta del término que puede resultar interesante es:
Por lo tanto, dudaría en utilizar el término "inseguro" para referirme a una implementación de lenguaje de programación. Si un término indefinido en un lenguaje produce un comportamiento diferente en diferentes implementaciones, una de las implementaciones podría generar un comportamiento más esperado, pero no lo llamaría "seguro".
fuente
Seguro no es binario, es un continuo .
Hablando informalmente, la seguridad se entiende por oposición a los errores, siendo los 2 mencionados con mayor frecuencia:
Esas no son las únicas clases de errores que los idiomas evitan, la libertad de carrera de datos o la libertad de punto muerto son bastante deseables, las pruebas de corrección son bastante dulces, etc.
Sin embargo, los programas simplemente incorrectos rara vez se consideran "inseguros" (solo con errores), y el término seguridad generalmente se reserva para las garantías que afectan nuestra capacidad de razonar sobre un programa. Por lo tanto, C, C ++ o Go, que tienen un comportamiento indefinido, no son seguros.
Y, por supuesto, hay idiomas con subconjuntos inseguros (Java, Rust, ...) que delimitan áreas a propósito donde el desarrollador es responsable de mantener las garantías del idioma y el compilador está en modo "no intervenido". Los idiomas todavía se denominan generalmente seguros , a pesar de esta escotilla de escape, una definición pragmática.
fuente
Obj.magic
en Ocaml). Y en la práctica, estos son realmente necesariosSi bien no estoy en desacuerdo con la respuesta de DW, creo que deja una parte de "seguro" sin abordar.
Como se señaló, hay múltiples tipos de seguridad promovida. Creo que es bueno entender por qué hay múltiples nociones. Cada noción está asociada con la idea de que los programas sufren especialmente de una cierta clase de errores, y que los programadores no podrían hacer este tipo específico de error si el lenguaje bloqueara al programador.
Cabe señalar que, por lo tanto, estas nociones diferentes tienen diferentes clases de errores, y estas clases no son mutuamente excluyentes ni cubren todas las formas de errores. Solo para tomar los 2 ejemplos de DW, la pregunta de si una determinada ubicación de memoria contiene un determinado objeto es tanto una cuestión de tipo de seguridad como de seguridad de la memoria.
Otra crítica de los "lenguajes seguros" se desprende de la observación de que la prohibición de ciertas construcciones consideradas peligrosas deja al programador con la necesidad de encontrar alternativas. Empíricamente, la seguridad se logra mejor con buenas bibliotecas. El uso de código que ya ha sido probado en el campo le ahorra la creación de nuevos errores.
fuente
Una diferencia fundamental entre C y Java es que si se evitan ciertas características de Java fácilmente identificables (por ejemplo, aquellas en el
Unsafe
espacio de nombres), cada acción posible que se pueda intentar, incluidas las "erróneas", tendrá un rango limitado de resultados posibles . Si bien esto limita lo que se puede hacer en Java, al menos sin usar elUnsafe
espacio de nombres, también permite limitar el daño que puede causar un programa erróneo o, lo que es más importante, un programa que procese correctamente archivos válidos pero no está especialmente protegido contra archivos erróneos.Tradicionalmente, los compiladores de C procesaban muchas acciones de forma estándar en casos "normales", mientras que procesaban muchos casos de esquina "de una manera característica del entorno". Si uno usara una CPU que se cortocircuitaría y se incendiaría si ocurriera un desbordamiento numérico y quisiera evitar que la CPU se incendie, necesitaría escribir un código para evitar el desbordamiento numérico. Sin embargo, si uno usara una CPU que truncaría perfectamente los valores de manera complementaria, no tendría que evitar desbordamientos en los casos en que dicho truncamiento resultaría en un comportamiento aceptable.
Modern C lleva las cosas un paso más allá: incluso si uno apunta a una plataforma que naturalmente definiría un comportamiento para algo como el desbordamiento numérico donde el Estándar no impondría requisitos, el desbordamiento en una parte de un programa puede afectar el comportamiento de otras partes del programa de manera arbitraria no vinculada por las leyes del tiempo y la causalidad. Por ejemplo, considere algo como:
Un compilador de C "moderno" dado algo como lo anterior podría concluir que, dado que el cálculo de x * x se desbordaría si x es mayor que 46340, puede realizar la llamada a "foo" incondicionalmente. Tenga en cuenta que incluso si fuera aceptable que un programa terminara anormalmente si x está fuera de rango, o que la función devuelva cualquier valor en tales casos, llamar a foo () con un fuera de rango x podría causar daños mucho más allá Cualquiera de esas posibilidades. C tradicional no proporcionaría ningún equipo de seguridad más allá de lo que el programador y la plataforma subyacente suministraron, pero permitiría que el equipo de seguridad limite el daño de situaciones inesperadas. Modern C evitará cualquier equipo de seguridad que no sea 100% efectivo para mantener todo bajo control.
fuente
int
es de 32 bits,x
se promocionará a firmadoint
. A juzgar por la razón, los autores de la Norma espera que las implementaciones no extraños tratarían firmado y los tipos sin signo en el exterior de manera equivalente a algunos casos específicos, pero gcc veces "optimiza" a fin de romper si unuint16_t
poruint16_t
rendimientos multiplicar un resultado fuera de INT_MAX , incluso cuando el resultado se utiliza como un valor sin signo.-Wconversion
.return x+1;
que no deberían ser problemáticas, y enviar el resultado a uint32_t sofocaría el mensaje sin solucionar el problema.Hay varias capas de corrección en un idioma. En orden de abstracción creciente:
En el siguiente nivel, los errores detectados en tiempo de compilación en lugar de en tiempo de ejecución hacen que el lenguaje sea más seguro. Un programa sintácticamente correcto también debe ser semánticamente correcto tanto como sea posible. Por supuesto, el compilador no puede conocer el panorama general, por lo que esto se refiere al nivel de detalle. Los tipos de datos fuertes y expresivos son un aspecto de la seguridad en este nivel. Se podría decir que el lenguaje debería dificultar cometer ciertos tipos de errores(errores de tipo, acceso fuera de límite, variables no inicializadas, etc.). La información de tipo de tiempo de ejecución, como las matrices que transportan información de longitud, evita errores. Programé Ada 83 en la universidad y descubrí que un programa de compilación de Ada generalmente contenía quizás un orden de magnitud menos errores que el programa C correspondiente. Simplemente aproveche la capacidad de Ada para definir tipos enteros que no sean asignables sin conversión explícita: las naves espaciales enteras se han estrellado porque los pies y los metros estaban confundidos, lo que uno podría evitar trivialmente con Ada.
En el siguiente nivel, el lenguaje debe proporcionar medios para evitar el código repetitivo. Si tiene que escribir sus propios contenedores, o su clasificación, o su concatenación, o si debe escribir los suyos
string::trim()
, cometerá errores. Dado que el nivel de abstracción aumenta, este criterio implica el lenguaje propiamente dicho, así como la biblioteca estándar del lenguaje.En estos días, el lenguaje debe proporcionar medios para la programación concurrente en el nivel del lenguaje. La simultaneidad es difícil de corregir y tal vez imposible de hacer correctamente sin el soporte de idiomas.
El lenguaje debe proporcionar medios para la modularización y la colaboración. Los tipos fuertes, elaborados y definidos por el usuario de arriba ayudan a crear API expresivas.
Algo ortogonalmente, la definición del lenguaje debería ser inteligible; El lenguaje y las bibliotecas deben estar bien documentados. La documentación incorrecta o faltante conduce a programas incorrectos o incorrectos.
1 Pero debido a que, por lo general, la corrección de la máquina virtual no se puede probar, dichos lenguajes pueden, paradójicamente, no ser adecuados para requisitos de seguridad muy estrictos.
fuente
Cada idioma que conozco tiene formas de escribir programas ilegales que se pueden (compilar y) ejecutar. Y cada idioma que conozco tiene un subconjunto seguro. Entonces, ¿cuál es tu pregunta realmente?
La seguridad es multidimensional y subjetiva.
Algunos idiomas tienen muchas operaciones que son "inseguras". Otros tienen menos operaciones de este tipo. En algunos idiomas, la forma predeterminada de hacer algo es inherentemente insegura. En otros, la forma predeterminada es segura. En algunos idiomas, hay un subconjunto explícito "inseguro". En otros idiomas, no existe tal subconjunto en absoluto.
En algunos idiomas, "seguridad" se refiere exclusivamente a la seguridad de la memoria, un servicio ofrecido por la biblioteca estándar y / o el tiempo de ejecución donde las infracciones de acceso a la memoria se hacen difíciles o imposibles. En otros idiomas, "seguridad" incluye explícitamente la seguridad de subprocesos. En otros idiomas, "seguridad" se refiere a la garantía de que un programa no se bloqueará (un requisito que incluye no permitir excepciones no detectadas de ningún tipo). Finalmente, en muchos idiomas "seguridad" se refiere a la seguridad de tipos: si el sistema de tipos es consistente de cierta manera, se dice que es "sólido" (por cierto, Java y C # no tienen sistemas de tipos completamente sólidos).
Y en algunos idiomas, todos los diferentes significados de "seguridad" se consideran subconjuntos de seguridad de tipo (por ejemplo, Rust y Pony logran la seguridad del hilo a través de las propiedades del sistema de tipo).
fuente
Esta respuesta es un poco más amplia. Las palabras seguridad y protección han sido mutiladas en las últimas décadas por ciertas partes políticamente orientadas de la sociedad de habla inglesa, de modo que su uso común casi no tiene definición. Sin embargo, para temas técnicos, todavía vuelvo a definir "seguridad" y "seguro" como: un dispositivo que evita el uso involuntario de algo o que hace que el uso accidental sea sustancialmente más difícil, y el estado de estar bajo la protección de dicho dispositivo .
Entonces, un lenguaje seguro tiene algún dispositivo para limitar una clase particular de errores. Por supuesto, los límites vienen con inconvenientes o incluso incapacidad en algunos casos, y eso no quiere decir que los idiomas "inseguros" darán lugar a errores. Por ejemplo, no tengo corchos de seguridad en mis tenedores y durante décadas he logrado, sin mucho esfuerzo, evitar apuñalar mi ojo mientras como. Ciertamente menos esfuerzo del que se hubiera gastado usando los corchos. Entonces, la seguridad tiene un costo contra el cual debe juzgarse. (el tenedor de corcho es una referencia a un personaje de Steve Martin)
fuente