segmentation dumped c struct segmentation-fault compiler-optimization strcpy

segmentation fault(core dumped) python



strcpy()/strncpy() se bloquea en el miembro de la estructura con espacio adicional cuando la optimización está activada en Unix? (6)

Cuando escribía un proyecto, me encontré con un problema extraño.

Este es el código mínimo que logré escribir para recrear el problema. Estoy intencionalmente almacenando una cadena real en lugar de otra cosa, con suficiente espacio asignado.

// #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> // For offsetof() typedef struct _pack{ // The type of `c` doesn''t matter as long as it''s inside of a struct. int64_t c; } pack; int main(){ pack *p; char str[9] = "aaaaaaaa"; // Input size_t len = offsetof(pack, c) + (strlen(str) + 1); p = malloc(len); // Version 1: crash strcpy((char*)&(p->c), str); // Version 2: crash strncpy((char*)&(p->c), str, strlen(str)+1); // Version 3: works! memcpy((char*)&(p->c), str, strlen(str)+1); // puts((char*)&(p->c)); free(p); return 0; }

El código anterior me confunde:

  • Con gcc/clang -O0 , tanto strcpy() como memcpy() funcionan en Linux / WSL, y puts() continuación da lo que ingresé.
  • Con clang -O0 en OSX , el código se bloquea con strcpy() .
  • Con gcc/clang -O2 o gcc/clang -O2 en Ubuntu / Fedora / WSL , el código falla (!!) en strcpy() , mientras que memcpy() funciona bien.
  • Con gcc.exe en Windows, el código funciona bien sea cual sea el nivel de optimización.

También encontré algunos otros rasgos del código:

  • (Parece que) la entrada mínima para reproducir el bloqueo es de 9 bytes (incluido el cero terminador) o 1+sizeof(p->c) . Con esa longitud (o más), se garantiza un bloqueo (Dear me ...).
  • Incluso si asigno espacio extra (hasta 1MB) en malloc() , no ayuda. Los comportamientos anteriores no cambian en absoluto.
  • strncpy() comporta exactamente igual, incluso con la longitud correcta suministrada a su tercer argumento.
  • El puntero no parece importar. Si el miembro de estructura char *c se cambia en long long c (o int64_t ), el comportamiento sigue siendo el mismo. (Actualización: ya cambiado).
  • El mensaje de bloqueo no se ve con regularidad. Se proporciona mucha información adicional.

Probé todos estos compiladores y no hicieron ninguna diferencia:

  • GCC 5.4.0 (Ubuntu / Fedora / OS X / WSL, todos son de 64 bits)
  • GCC 6.3.0 (solo Ubuntu)
  • GCC 7.2.0 (Android, norepro ???) (Este es el GCC de C4droid )
  • Clang 5.0.0 (Ubuntu / OS X)
  • MinGW GCC 6.3.0 (Windows 7/10, ambos x64)

Además, esta función de copia de cadenas personalizada, que se ve exactamente como la estándar, funciona bien con cualquier configuración de compilador mencionada anteriormente:

char* my_strcpy(char *d, const char* s){ char *r = d; while (*s){ *(d++) = *(s++); } *d = ''/0''; return r; }

Preguntas:

  • ¿Por qué strcpy() falla? ¿Cómo puede?
  • ¿Por qué falla solo si la optimización está activada?
  • ¿Por qué no memcpy() independientemente de -O nivel?

* Si desea debatir acerca de la violación de acceso de miembro de struct, relájese here .

Parte de la salida de objdump -d de un ejecutable bloqueado (en WSL):

PD Inicialmente quiero escribir una estructura, cuyo último elemento es un puntero a un espacio asignado dinámicamente (para una cadena). Cuando escribo la estructura en el archivo, no puedo escribir el puntero. Debo escribir la cadena actual. Así que se me ocurrió esta solución: almacenar una cuerda en el lugar de un puntero.

Además, por favor, no te quejes acerca de gets() . No lo uso en mi proyecto, pero solo el código de ejemplo anterior.


¿Por qué complicar las cosas? Sobrecomplexificar como lo está haciendo le da más espacio para un comportamiento indefinido , en esa parte:

memcpy((char*)&p->c, str, strlen(str)+1); puts((char*)&p->c);

advertencia: pasando el argumento 1 de ''puts'' desde el puntero incompatible ty pe [-Wcomcompatible-pointer-types] puts (& p-> c);

está claramente terminando en un área de memoria no asignada o en algún lugar modificable si tiene suerte ...

Optimizar o no puede cambiar los valores de las direcciones, y puede funcionar (dado que las direcciones coinciden), o no. Simplemente no puedes hacer lo que quieres hacer (básicamente mentirle al compilador )

Me gustaría:

  • asigna lo que se necesita para la estructura, no tomes en cuenta la longitud de la cadena, es inútil
  • no use, ya que es inseguro y obsoleto
  • use strdup lugar del código memcpy propenso a memcpy que está utilizando ya que está manejando cadenas. strdup no se olvidará de asignar el nul-terminator, y lo establecerá en el objetivo para usted.
  • no olvides liberar la cadena duplicada
  • lea las advertencias, put(&p->c) es un comportamiento indefinido

test.c: 19: 10: warning: pasa el argumento 1 de ''puts'' desde el puntero incompatible ty pe [-Wcomcompatible-pointer-types] puts (& p-> c);

Mi propuesta

int main(){ pack *p = malloc(sizeof(pack)); char str[1024]; fgets(str,sizeof(str),stdin); p->c = strdup(str); puts(p->c); free(p->c); free(p); return 0; }


Lo que estás haciendo es un comportamiento indefinido.

El compilador puede suponer que nunca usará más que sizeof int64_t para el miembro variable int64_t c . Por lo tanto, si intenta escribir más que sizeof int64_t (también conocido como sizeof c ) en c , tendrá un problema fuera de límites en su código. Este es el caso porque sizeof "aaaaaaaa" > sizeof int64_t .

El punto es que, incluso si asigna el tamaño de memoria correcto usando malloc() , el compilador puede suponer que nunca usará más que sizeof int64_t en su sizeof int64_t strcpy() o memcpy() . Porque envía la dirección de c (también int64_t c como int64_t c ).

TL; DR: Está intentando copiar 9 bytes a un tipo que consta de 8 bytes (suponemos que un byte es un octeto). (Desde @Kcvin )

Si desea algo similar, use los miembros de matriz flexible de C99:

#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { size_t size; char str[]; } string; int main(void) { char str[] = "aaaaaaaa"; size_t len_str = strlen(str); string *p = malloc(sizeof *p + len_str + 1); if (!p) { return 1; } p->size = len_str; strcpy(p->str, str); puts(p->str); strncpy(p->str, str, len_str + 1); puts(p->str); memcpy(p->str, str, len_str + 1); puts(p->str); free(p); }

Nota: Para una cotización estándar, consulte this respuesta.


Ninguna respuesta ha hablado aún en detalle sobre por qué este código puede ser o no un comportamiento indefinido.

El estándar no está especificado en esta área, y hay una propuesta activa para solucionarlo. Según esa propuesta, este código NO sería un comportamiento indefinido, y los compiladores que generan código que falla no cumplirían con el estándar actualizado. (Vuelvo a visitar esto en mi párrafo final a continuación).

Pero tenga en cuenta que, según la discusión de -D_FORTIFY_SOURCE=2 en otras respuestas, parece que este comportamiento es intencional por parte de los desarrolladores involucrados.

Hablaré basado en el siguiente fragmento:

char *x = malloc(9); pack *y = (pack *)x; char *z = (char *)&y->c; char *w = (char *)y;

Ahora, los tres de x z w refieren a la misma ubicación de memoria, y tendrían el mismo valor y la misma representación. Pero el compilador trata z diferente a x . (El compilador también trata w diferente a uno de esos dos, aunque no sabemos cuál como OP no exploró ese caso).

Este tema se llama procedencia de puntero . Significa la restricción sobre qué objeto puede oscilar el valor de un puntero. El compilador toma z como de procedencia solo en y->c , mientras que x tiene procedencia en toda la asignación de 9 bytes.

El estándar C actual no especifica muy bien la procedencia. Las reglas como la resta del puntero solo pueden aparecer entre dos punteros al mismo objeto de matriz es un ejemplo de una regla de procedencia. Otra regla de procedencia es la que se aplica al código que estamos discutiendo, C 6.5.6 / 8:

Cuando una expresión que tiene un tipo de entero se agrega o se resta de un puntero, el resultado tiene el tipo del operando del puntero. Si el operando puntero apunta a un elemento de un objeto de matriz, y la matriz es lo suficientemente grande, el resultado apunta a un desplazamiento de elemento desde el elemento original tal que la diferencia de los subíndices de los elementos de matriz resultante y original es igual a la expresión entera. En otras palabras, si la expresión P apunta al i -ésimo elemento de un objeto de matriz, las expresiones (P)+N (equivalentemente, N+(P) ) y (P)-N (donde N tiene el valor n ) señalan a, respectivamente, los elementos i+n -ésimo e i−n ésimo del objeto de matriz, siempre que existan. Además, si la expresión P apunta al último elemento de un objeto de matriz, la expresión (P)+1 señala uno pasado el último elemento del objeto de matriz, y si la expresión Q señala un último elemento de un objeto de matriz, la expresión (Q)-1 apunta al último elemento del objeto de la matriz. Si tanto el operando del puntero como el resultado apuntan a elementos del mismo objeto del arreglo, o uno más allá del último elemento del objeto del arreglo, la evaluación no producirá un desbordamiento; de lo contrario, el comportamiento no está definido. Si el resultado señala uno pasado el último elemento del objeto de la matriz, no se utilizará como el operando de un operador unario * que se evalúa.

La justificación para la comprobación de límites de strcpy , memcpy siempre vuelve a esta regla: esas funciones se definen para comportarse como si fueran una serie de asignaciones de caracteres desde un puntero base que se incrementa para llegar al siguiente carácter, y el incremento de un el puntero está cubierto por (P)+1 como se describe en esta regla.

Tenga en cuenta que el término "objeto de matriz" puede aplicarse a un objeto que no fue declarado como una matriz. Esto se explica en 6.5.6 / 7:

Para los propósitos de estos operadores, un puntero a un objeto que no es un elemento de una matriz se comporta de la misma manera que un puntero al primer elemento de una matriz de longitud uno con el tipo de objeto como su tipo de elemento.

La gran pregunta aquí es: ¿qué es "el objeto matriz" ? En este código, ¿es y->c , *y , o el objeto real de 9 bytes devuelto por malloc?

Fundamentalmente, el estándar no arroja ninguna luz sobre este asunto. Siempre que tengamos objetos con subobjetos, el estándar no dice si 6.5.6 / 8 se refiere al objeto o al subobjeto.

Un factor de complicación adicional es que el estándar no proporciona una definición para "matriz" ni para "objeto de matriz". Pero para abreviar, el objeto asignado por malloc se describe como "una matriz" en varios lugares en el estándar, por lo que parece que el objeto de 9 bytes aquí es un candidato válido para "el objeto de matriz". (De hecho, este es el único candidato para el caso de usar x para iterar sobre la asignación de 9 bytes, que creo que todos estarían de acuerdo es legal).

Nota: esta sección es muy especulativa e intento proporcionar un argumento sobre por qué la solución elegida por los compiladores aquí no es auto consistente

Se podría argumentar que &y->c significa que la procedencia es el subobjeto int64_t . Pero esto conduce inmediatamente a la dificultad. Por ejemplo, ¿tiene y la procedencia de *y ? Si es así, (char *)y debería tener la procedencia *y todavía, pero esto contradice la regla de 6.3.2.3/7 que arroja un puntero a otro tipo y el reverso debe devolver el puntero original (siempre que la alineación no sea violado).

Otra cosa que no cubre es la proveniencia superpuesta. ¿Puede un puntero comparar desigual a un puntero del mismo valor pero una procedencia más pequeña (que es un subconjunto de la procedencia más grande)?

Además, si aplicamos ese mismo principio al caso donde el subobjeto es una matriz:

char arr[2][2]; char *r = (char *)arr; ++r; ++r; ++r; // undefined behavior - exceeds bounds of arr[0]

arr se define como significado &arr[0] en este contexto, por lo que si la procedencia de &X es X , entonces r está realmente limitada a solo la primera fila de la matriz, lo que tal vez sea un resultado sorprendente.

Sería posible decir que char *r = (char *)arr; conduce a UB aquí, pero char *r = (char *)&arr; no. De hecho, solía promocionar esta vista en mis publicaciones hace muchos años. Pero ya no lo hago: en mi experiencia de tratar de defender esta posición, simplemente no puede hacerse auto consistente, hay demasiados escenarios de problemas. E incluso si pudiera hacerse autoconsistente, el hecho es que el estándar no lo especifica. En el mejor de los casos, esta vista debe tener el estado de una propuesta.

Para finalizar, recomendaría leer N2090: Procedencia del puntero aclarante (Informe de borrador del defecto o Propuesta para C2x) .

Su propuesta es que la procedencia siempre se aplica a una asignación . Esto hace que todas las complejidades de objetos y subobjetos sean discutibles. No hay sub-asignaciones. En esta propuesta, todos los x z w son idénticos y se pueden usar para que abarquen toda la asignación de 9 bytes. En mi humilde opinión, la simplicidad de esto es atractiva, en comparación con lo que se discutió en mi sección anterior.


Reproduje este problema en mi Ubuntu 16.10 y encontré algo interesante.

Cuando se compila con gcc -O3 -o ./test ./test.c , el programa se bloqueará si la entrada es más larga que 8 bytes.

Después de invertir algo, descubrí que GCC reemplazó a strcpy con memcpy_chk , mira esto.

// decompile from IDA int __cdecl main(int argc, const char **argv, const char **envp) { int *v3; // rbx int v4; // edx unsigned int v5; // eax signed __int64 v6; // rbx char *v7; // rax void *v8; // r12 const char *v9; // rax __int64 _0; // [rsp+0h] [rbp+0h] unsigned __int64 vars408; // [rsp+408h] [rbp+408h] vars408 = __readfsqword(0x28u); v3 = (int *)&_0; gets(&_0, argv, envp); do { v4 = *v3; ++v3; v5 = ~v4 & (v4 - 16843009) & 0x80808080; } while ( !v5 ); if ( !((unsigned __int16)~(_WORD)v4 & (unsigned __int16)(v4 - 257) & 0x8080) ) v5 >>= 16; if ( !((unsigned __int16)~(_WORD)v4 & (unsigned __int16)(v4 - 257) & 0x8080) ) v3 = (int *)((char *)v3 + 2); v6 = (char *)v3 - __CFADD__((_BYTE)v5, (_BYTE)v5) - 3 - (char *)&_0; // strlen v7 = (char *)malloc(v6 + 9); v8 = v7; v9 = (const char *)_memcpy_chk(v7 + 8, &_0, v6 + 1, 8LL); // Forth argument is 8!! puts(v9); free(v8); return 0; }

Su paquete de estructura hace que GCC crea que el elemento c tiene exactamente 8 bytes de longitud.

¡Y memcpy_chk fallará si la longitud de copiado es mayor que el cuarto argumento!

Entonces hay 2 soluciones:

  • Modifica tu estructura

  • Usar las opciones de compilación -D_FORTIFY_SOURCE=0 (le gusta gcc test.c -O3 -D_FORTIFY_SOURCE=0 -o ./test ) para desactivar las funciones de fortalecimiento.

    Precaución : ¡Esto deshabilitará completamente la verificación de desbordamiento de búfer en todo el programa!


Su puntero p-> c es la causa del bloqueo.
Primero inicialice la estructura con el tamaño de "sin signo de larga duración" más el tamaño de "* p".
Segundo puntero inicializador p-> c con el tamaño de área requerido. Hacer copia de operación: strcpy (p-> c, str);
Finalmente libre primero libre (p-> c) y libre (p).
Creo que fue esto.
[EDITAR]
Insisto La causa del error es que su estructura solo reserva espacio para el puntero pero no asigna el puntero para contener los datos que se copiarán.
Echar un vistazo

int main() { pack *p; char str[1024]; gets(str); size_t len_struc = sizeof(*p) + sizeof(unsigned long long); p = malloc(len_struc); p->c = malloc(strlen(str)); strcpy(p->c, str); // This do not crashes! puts(&p->c); free(p->c); free(p); return 0; }

[EDIT2]
Esta no es una forma tradicional de almacenar datos, pero esto funciona:

pack2 *p; char str[9] = "aaaaaaaa"; // Input size_t len = sizeof(pack) + (strlen(str) + 1); p = malloc(len); // Version 1: crash strcpy((char*)p + sizeof(pack), str); free(p);


Todo esto se debe a que -D_FORTIFY_SOURCE=2 intencionalmente lo que decidió que no era seguro.

Algunas distribuciones crean gcc con -D_FORTIFY_SOURCE=2 habilitado de forma predeterminada. Algunos no. Esto explica todas las diferencias entre los diferentes compiladores. Probablemente los que no se cuelguen normalmente lo harán si -O3 -D_FORTIFY_SOURCE=2 su código con -O3 -D_FORTIFY_SOURCE=2 .

¿Por qué falla solo si la optimización está activada?

_FORTIFY_SOURCE requiere compilar con optimización ( -O ) para realizar un seguimiento del tamaño de los objetos a través de asignaciones / lanzamientos de punteros. Consulte las diapositivas de esta charla para obtener más información sobre _FORTIFY_SOURCE .

¿Por qué Strcpy () falla? ¿Cómo puede?

gcc llama a __memcpy_chk para strcpy solo con -D_FORTIFY_SOURCE=2 . Pasa 8 como el tamaño del objeto de destino, porque eso es lo que cree que quiere decir / lo que puede deducir del código fuente que le dio. El mismo trato para strncpy calling __strncpy_chk .

__memcpy_chk aborta a propósito. _FORTIFY_SOURCE puede ir más allá de las cosas que son UB en C y no permitir cosas que parecen potencialmente peligrosas . Esto le da licencia para decidir que su código no es seguro. (Como otros han señalado, un miembro de matriz flexible como el último miembro de su estructura, y / o una unión con un miembro de matriz flexible, es cómo debe expresar lo que está haciendo en C.)

gcc incluso advierte que el cheque siempre fallará:

In function ''strcpy'', inlined from ''main'' at <source>:18:9: /usr/include/x86_64-linux-gnu/bits/string3.h:110:10: warning: call to __builtin___memcpy_chk will always overflow destination buffer return __builtin___strcpy_chk (__dest, __src, __bos (__dest)); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

(desde gcc7.2 -O3 -Wall en el explorador del compilador Godbolt ).

¿Por qué no memcpy() independientemente del nivel -O ?

IDK.

gcc lo completa completamente con solo 8B de carga / almacenamiento + 1B de carga / almacenamiento. (Parece una optimización perdida, debe saber que malloc no lo modificó en la pila, por lo que podría simplemente almacenarlo de inmediato en lugar de volver a cargarlo (o mejor mantener el valor de 8B en un registro).