Cómo escribir una prueba para cubrir un bugfix de un segfault

7

Hubo un error que reparé recientemente que estaba causando un error de segmentación (debido a un acceso fuera de los límites). El responsable del proyecto me está pidiendo que escriba una prueba de unidad para cubrir la corrección de errores.

¿Cómo puedo hacer esto? El error exhibió un comportamiento indefinido, así que incluso si mi prueba fue aprobada, no es una prueba de nada. ¿Debo usar un bucle para probarlo 100 veces para que la prueba tenga más probabilidades de contraer una regresión?

    
pregunta Garrett 14.06.2015 - 06:16

4 respuestas

11

El enfoque más pragmático es IMHO al no adherirse ciegamente al modelo de "no cambiar el código del proyecto bajo ninguna circunstancia". Así que mi sugerencia es: haga que el código sea verificable primero : cambie el código del proyecto ligeramente para asegurarse de que el acceso fuera de los límites lleve a un error reproducible y detectable. Tenga en cuenta que hacer que el código sea verificable es a menudo un requisito previo para cualquier tipo de automatización de prueba.

Esa modificación podría ser algún tipo de afirmación fuera de los límites, utilizando el tipo de señal de error que su equipo normalmente usa dentro de su proyecto (un código de error, lanzar una excepción, llamar a una señal o cualquier otra cosa que prefiera), pero debería permitir que su prueba automática detecte la condición de error. Además, tenga en cuenta que este cambio no es el mismo "corregir el error": la corrección de errores probablemente debería conducir a un código que ya no crea esa condición fuera de límites.

Luego, debe aplicar ese cambio al código original antes de la corrección de errores (si tiene suerte, su sistema VCS le ayudará a revertir el orden en el que se aplican los cambios). Finalmente, puede hacer exactamente lo que escribió Robert Harvey: escriba una prueba que haga que aparezca el error, y ejecute la prueba después . Hizo que el código fuera verificable, una vez antes y una vez después de la corrección de errores.

    
respondido por el Doc Brown 14.06.2015 - 07:30
4

(1)

Hay un concepto de "prueba de muerte" , en el que una pieza fallida de El código causaría la terminación del proceso.

Para ejecutar dicha prueba de muerte, el arnés de prueba debe iniciar un proceso separado que ejecutará la pieza de código. La condición de terminación se toma como criterio de aprobación o rechazo.

Obviamente, no todos los marcos de prueba de unidades admiten este tipo de pruebas.

(2)

Algunos compiladores de C ++ proporcionan conmutadores que insertarán controles adicionales en el código generado.

Ejemplos de lo que harán estos interruptores:

  1. Asegúrese de que cuando se devuelve la llamada a la función, su dirección de retorno tenga el mismo valor (en la pila) que cuando se ingresó la llamada a la función.
  2. Asegúrese de que los búferes de memoria asignados dinámicamente no se sobrescribieran antes de su inicio o después de su finalización.

(3)

Finalmente, también hay herramientas de instrumentación y cobertura de código como valgrind.

Si existen algunos patrones de datos que causarán una falla de manera confiable (pero simplemente no se sabe cuál), la confusión puede ser una estrategia muy efectiva para descubrir dichos patrones de datos que causan fallas. P.ej. afl-fuzz .

Si el uso de herramientas adicionales está fuera de cuestión, quizás pueda modificar el código usted mismo para que sea más "comprobable". Un ejemplo es reemplazar los accesos de la matriz con una sobrecarga del operador de C ++ que realiza la verificación de los límites del índice de la matriz. Los detalles dependen del tipo de código que se cuestiona.

    
respondido por el rwong 14.06.2015 - 07:10
2

Escriba una prueba que reproduzca de manera confiable la falla de seguridad en el código original primero. Debería poder duplicar el segfault escribiendo una prueba que provoque el acceso fuera del límite que usted reparó. Puede utilizar la misma prueba para demostrar que el acceso fuera de límite ya no se produce en el nuevo código.

    
respondido por el Robert Harvey 14.06.2015 - 06:40
1

Si su idioma admite comprobaciones de límites, actívelos para realizar pruebas. Esa es ciertamente la forma más fácil y segura de hacer esto. Como alguien sugirió en los comentarios, ¿por qué no declara @cython.boundscheck(isDebug) donde isDebug se define al principio del módulo (nunca he trabajado con cython, así que no estoy seguro de que sea tan fácil)

Dicho esto, hay casos en los que no puedes simplemente activar las comprobaciones de límites (ejemplo: estás utilizando una biblioteca de bajo nivel como LAPACK o IPP que solo toma un búfer y espera que tenga el tamaño correcto). La única forma que conozco para garantizar violaciones de acceso aquí es usar las funciones del sistema operativo de bajo nivel para asignar memoria de tal manera que la dirección inmediatamente anterior (o posterior) al bloque asignado esté protegida contra lectura y escritura. En Windows, VirtualAlloc , VirtualProtect y tus amigos te permiten hacer cosas así. Se desperdicia mucha memoria y solo puede proteger el espacio antes de o después del bloque (a menos que el tamaño del búfer sea un múltiplo de un tamaño de página de memoria). Pero para encontrar errores de acceso a la memoria fuera de límites difíciles de reproducir, a veces vale todo eso.

    
respondido por el nikie 14.06.2015 - 12:28

Lea otras preguntas en las etiquetas