Sexto control de cromo, epílogo

unicornio severo

A principios de 2018, nuestro blog se complementó con una serie de artículos sobre la sexta verificación del código fuente del proyecto Chromium. La serie incluye 8 artículos sobre errores y recomendaciones para su prevención. Dos artículos provocaron una acalorada discusión, y todavía ocasionalmente recibo comentarios por correo sobre los temas tratados en ellos. Tal vez, debería dar explicaciones adicionales y, como dicen, dejar las cosas claras.

Ha pasado un a√Īo desde que escribi√≥ una serie de art√≠culos sobre una verificaci√≥n regular del c√≥digo fuente del proyecto Chromium:

  1. Cromo: el sexto control del proyecto y 250 errores
  2. Bonito cromo y torpe memoria
  3. ruptura y caída
  4. Cromo: pérdidas de memoria
  5. Cromo: errores tipogr√°ficos
  6. Cromo: uso de datos no confiables
  7. Por qué es importante verificar qué devolvió la función malloc
  8. Cromo: otros errores

Los art√≠culos dedicados a memset y malloc han causado y contin√ļan causando debates, lo que me parece extra√Īo. Aparentemente, hubo cierta confusi√≥n debido al hecho de que no hab√≠a sido lo suficientemente preciso al verbalizar mis pensamientos. Decid√≠ volver a esos art√≠culos y hacer algunas aclaraciones.

memset


Comencemos con un artículo sobre memset , porque aquí todo es simple. Aparecieron algunos argumentos sobre la mejor manera de inicializar estructuras. Muchos programadores escribieron que sería mejor dar la recomendación de no escribir:

HDHITTESTINFO hhti = {}; 

pero para escribir de la siguiente manera:

 HDHITTESTINFO hhti = { 0 }; 

Razones:

  1. La construcción {0} es más fácil de notar al leer el código, que {}.
  2. La construcción {0} es más intuitivamente comprensible que {}. Lo que significa que 0 sugiere que la estructura está llena de ceros.

En consecuencia, los lectores me sugieren cambiar este ejemplo de inicialización en el artículo. No estoy de acuerdo con los argumentos y no planeo hacer ninguna edición en el artículo. Ahora voy a explicar mi opinión y dar algunas razones.

En cuanto a la visibilidad, creo que es cuestión de gustos y hábitos. No creo que la presencia de 0 entre paréntesis cambie fundamentalmente la situación.

En cuanto al segundo argumento, estoy totalmente en desacuerdo con √©l. El registro del tipo {0} da una raz√≥n para percibir incorrectamente el c√≥digo. Por ejemplo, puede suponer que si reemplaza 0 con 1, todos los campos se inicializar√°n con unos. Por lo tanto, es m√°s probable que dicho estilo de escritura sea perjudicial en lugar de √ļtil.

El analizador PVS-Studio incluso tiene un diagnóstico relacionado V1009 , cuya descripción se cita a continuación.

V1009. Verifique la inicialización de la matriz. Solo el primer elemento se inicializa explícitamente.

El analizador ha detectado un posible error relacionado con el hecho de que al declarar una matriz, el valor se especifica solo para un elemento. Por lo tanto, los elementos restantes se inicializarán implícitamente por cero o por un constructor predeterminado.

Consideremos el ejemplo de código sospechoso:

 int arr[3] = {1}; 

Quizás el programador esperaba que arr consistiría completamente en unos, pero no lo es. La matriz constará de los valores 1, 0, 0.

Código correcto:

 int arr[3] = {1, 1, 1}; 

Tal confusión puede ocurrir debido a la similitud con la construcción arr = {0} , que inicializa toda la matriz con ceros.

Si tales construcciones se utilizan activamente en su proyecto, puede deshabilitar este diagnóstico.

También recomendamos no descuidar la claridad de su código.

Por ejemplo, el código para codificar valores de un color se registra de la siguiente manera:

 int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00 }; int Green[3] = { 0x00, 0xff }; 

Gracias a la inicialización implícita, todos los colores se especifican correctamente, pero es mejor reescribir el código más claramente:

 int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00, 0x00, 0x00 }; int Green[3] = { 0x00, 0xff, 0x00 }; 

malloc


Antes de seguir leyendo, recuerde el contenido del artículo " Por qué es importante verificar qué devolvió la función malloc ". Este artículo ha dado lugar a mucho debate y crítica. Estas son algunas de las discusiones: reddit.com/r/cpp , reddit.com/r/C_Programming , habr.com (en). Ocasionalmente, los lectores todavía me envían correos electrónicos sobre este artículo.

El artículo es criticado por los lectores por los siguientes puntos:

1. Si malloc devolvió NULL , entonces es mejor terminar inmediatamente el programa, que escribir un montón de if -s e intentar manejar de alguna manera la memoria, debido a lo cual la ejecución del programa es frecuentemente imposible de todos modos.

No he presionado para luchar hasta el final con las consecuencias de la pérdida de memoria, pasando el error cada vez más alto. Si se permite que su aplicación termine su trabajo sin previo aviso, entonces que así sea. Para este propósito, incluso una sola comprobación justo después de malloc o usando xmalloc es suficiente (vea el siguiente punto).

Me opuse y advert√≠ sobre la falta de controles debido a que el programa contin√ļa funcionando como si nada hubiera pasado. Es un caso completamente diferente. Es peligroso, ya que conduce a un comportamiento indefinido, corrupci√≥n de datos, etc.

2. No existe una descripción de una solución que se base en escribir funciones de envoltura para asignar memoria con una verificación a continuación o usar funciones ya existentes, como xmalloc .

De acuerdo, perdí este punto. Al escribir el artículo simplemente no estaba pensando en la forma de remediar la situación. Para mí era más importante transmitirle al lector el peligro de la ausencia de cheques. Cómo solucionar un error es una cuestión de gusto y detalles de implementación.

La funci√≥n xmalloc no forma parte de la biblioteca C est√°ndar (consulte " ¬ŅCu√°l es la diferencia entre xmalloc y malloc? "). Sin embargo, esta funci√≥n puede declararse en otras bibliotecas, por ejemplo, en la biblioteca de utilidades de GNU ( libiberty de GNU ).

El punto principal de la función es que el programa se bloquea cuando no puede asignar memoria. La implementación de esta función podría tener el siguiente aspecto:

 void* xmalloc(size_t s) { void* p = malloc(s); if (!p) { fprintf (stderr, "fatal: out of memory (xmalloc(%zu)).\n", s); exit(EXIT_FAILURE); } return p; } 

En consecuencia, al llamar a una función xmalloc en lugar de malloc cada vez, puede estar seguro de que no se producirá un comportamiento indefinido en el programa debido al uso de un puntero nulo.

Desafortunadamente, xmalloc tampoco es una panacea. Uno debe recordar que el uso de xmalloc es inaceptable cuando se trata de escribir código de bibliotecas. Hablaré de eso más tarde.

3. La mayoría de los comentarios fueron los siguientes: "en la práctica, malloc nunca devuelve NULL ".

Afortunadamente, no soy el √ļnico que entiende que este es el enfoque equivocado. Realmente me gust√≥ este comentario en mi soporte:

Seg√ļn mi experiencia en la discusi√≥n de este tema, tengo la sensaci√≥n de que hay dos sectas en Internet. Los partidarios del primero creen firmemente que malloc nunca devuelve NULL en Linux. Los partidarios del segundo afirman de todo coraz√≥n que si no se puede asignar memoria en su programa, no se puede hacer nada, solo puede fallar. No hay forma de persuadirlos demasiado. Especialmente cuando estas dos sectas se cruzan. Solo puedes tomarlo como un hecho. E incluso no es importante sobre qu√© recurso especializado tiene lugar una discusi√≥n.

Pensé por un momento y decidí seguir los consejos, así que no intentaré persuadir a nadie :). Con suerte, estos grupos de desarrolladores escriben solo programas no fatales. Si, por ejemplo, algunos datos en el juego se corrompen, no hay nada crucial en ellos.

Lo √ļnico que importa es que los desarrolladores de bibliotecas y bases de datos no deben hacer esto.

Apelar a los desarrolladores de código y bibliotecas altamente dependientes


Si está desarrollando una biblioteca u otro código altamente dependiente, siempre verifique el valor del puntero devuelto por la función malloc / realloc y devuelva un código de error si la memoria no se puede asignar.

En las bibliotecas, no puede llamar a la funci√≥n de salida , si falla la asignaci√≥n de memoria. Por la misma raz√≥n, no puedes usar xmalloc . Para muchas aplicaciones, es inaceptable simplemente abortarlas. Debido a esto, por ejemplo, una base de datos puede estar da√Īada. Se pueden perder datos que se evaluaron durante muchas horas. Debido a esto, el programa puede alcanzar vulnerabilidades de "denegaci√≥n de servicio" cuando, en lugar de manejar correctamente la creciente carga de trabajo, una aplicaci√≥n multiproceso simplemente finaliza.

No se puede suponer, de qu√© maneras y en qu√© proyectos se utilizar√° la biblioteca. Por lo tanto, se debe suponer que la aplicaci√≥n puede resolver tareas muy cr√≠ticas. Es por eso que matarlo llamando a exit no es bueno. Lo m√°s probable es que dicho programa est√© escrito teniendo en cuenta la posibilidad de falta de memoria y puede hacer algo en este caso. Por ejemplo, un sistema CAD no puede asignar un b√ļfer de memoria apropiado que sea suficiente para la operaci√≥n regular debido a la fuerte fragmentaci√≥n de la memoria. En este caso, no es la raz√≥n por la que se aplasta en el modo de emergencia con p√©rdida de datos. El programa puede proporcionar una oportunidad para guardar el proyecto y reiniciarse normalmente.

En ning√ļn caso es imposible confiar en malloc que siempre podr√° asignar memoria. No se sabe en qu√© plataforma y c√≥mo se utilizar√° la biblioteca. Si la situaci√≥n de poca memoria en una plataforma es ex√≥tica, puede ser una situaci√≥n bastante com√ļn en la otra.

No podemos esperar que si malloc devuelve NULL , el programa se bloquear√°. Cualquier cosa puede pasar. Como describ√≠ en el art√≠culo , el programa puede escribir datos no por la direcci√≥n nula. Como resultado, algunos datos pueden estar da√Īados, lo que lleva a consecuencias impredecibles. Incluso memset es peligroso. Si el relleno con datos va en orden inverso, primero algunos datos se corrompen y luego el programa se bloquear√°. Pero el choque puede ocurrir demasiado tarde. Si los datos contaminados se utilizan en subprocesos paralelos mientras la funci√≥n memset funciona, las consecuencias pueden ser fatales. Puede obtener una transacci√≥n corrupta en una base de datos o enviar comandos para eliminar archivos "innecesarios". Cualquier cosa tiene la posibilidad de suceder. Sugiero a un lector que sue√Īe con lo que podr√≠a suceder debido al uso de basura en la memoria.

Por lo tanto, la biblioteca solo tiene una forma correcta de trabajar con las funciones malloc . Debe comprobar INMEDIATAMENTE que la función regresó y, si es NULL, devolver un estado de error.

Enlaces adicionales


  1. Manejo de OOM
  2. Diversión con punteros NULL: parte 1 , parte 2
  3. Lo que todo programador de C debe saber sobre el comportamiento indefinido: parte 1 , parte 2 , parte 3

Source: https://habr.com/ru/post/438756/


All Articles