Tengo un paquete R con código compilado en C que ha sido relativamente estable durante bastante tiempo y se prueba con frecuencia en una amplia variedad de plataformas y compiladores (windows/osx/debian/fedora gcc/clang).
Más recientemente, se agregó una nueva plataforma para probar el paquete nuevamente:
Logs from checks with gcc trunk aka 10.0.1 compiled from source on Fedora 30. (For some archived packages, 10.0.0.) x86_64 Fedora 30 Linux FFLAGS="-g -O2 -mtune=native -Wall -fallow-argument-mismatch" CFLAGS="-g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection" CXXFLAGS="-g -O2 -Wall -pedantic -mtune=native -Wno-ignored-attributes -Wno-deprecated-declarations -Wno-parentheses -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection"En ese momento, el código compilado comenzó rápidamente a fallar en el segmento de la siguiente manera:
*** caught segfault *** address 0x1d00000001, cause 'memory not mapped' Pude reproducir la falla de segmento de manera consistente usando el contenedor acoplable rocker/r-base con gcc-10.0.1 con el nivel de optimización -O2 . Ejecutar una optimización más baja elimina el problema. Ejecutar cualquier otra configuración, incluso bajo valgrind (tanto -O0 como -O2), UBSAN (gcc/clang), no muestra ningún problema. También estoy razonablemente seguro de que esto se ejecutó con gcc-10.0.0 , pero no tengo los datos.
Ejecuté la versión gcc-10.0.1 -O2 con gdb y noté algo que me parece extraño:
Mientras recorre la sección resaltada, parece que se omite la inicialización de los segundos elementos de las matrices ( R_alloc es un envoltorio alrededor de malloc que se recolecta automáticamente cuando se devuelve el control a R; la falla de segmento ocurre antes de regresar a R). Posteriormente, el programa falla cuando se accede al elemento no inicializado (en la versión gcc.10.0.1 -O2).
Solucioné esto inicializando explícitamente el elemento en cuestión en todas partes del código que finalmente condujo al uso del elemento, pero realmente debería haberse inicializado en una cadena vacía, o al menos eso es lo que habría asumido.
¿Me estoy perdiendo algo obvio o estoy haciendo algo estúpido? Ambos son razonablemente probables ya que C es mi segundo idioma con diferencia . Es extraño que esto haya surgido ahora, y no puedo entender qué está tratando de hacer el compilador.
ACTUALIZACIÓN : Instrucciones para reproducir esto, aunque solo se reproducirá mientras debian:testing docker container tenga gcc-10 en gcc-10.0.1 . Además, no ejecute estos comandos si no confía en mí .
Lo siento, este no es un ejemplo mínimo reproducible.
docker pull rocker/r-base docker run --rm -ti --security-opt seccomp=unconfined \ rocker/r-base /bin/bash apt-get update apt-get install gcc-10 gdb gcc-10 --version # confirm 10.0.1 # gcc-10 (Debian 10-20200222-1) 10.0.1 20200222 (experimental) # [master revision 01af7e0a0c2:487fe13f218:e99b18cf7101f205bfdd9f0f29ed51caaec52779] mkdir ~/.R touch ~/.R/Makevars echo "CC = gcc-10 CFLAGS = -g -O2 -Wall -pedantic -mtune=native -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong -fstack-clash-protection -fcf-protection " >> ~/.R/Makevars R -d gdb --vanilla Luego, en la consola R, después de escribir run para que gdb ejecute el programa:
f.dl <- tempfile() f.uz <- tempfile() github.url <- 'https://github.com/brodieG/vetr/archive/v0.2.8.zip' download.file(github.url, f.dl) unzip(f.dl, exdir=f.uz) install.packages( file.path(f.uz, 'vetr-0.2.8'), repos=NULL, INSTALL_opts="--install-tests", type='source' ) # minimal set of commands to segfault library(vetr) alike(pairlist(a=1, b="character"), pairlist(a=1, b=letters)) alike(pairlist(1, "character"), pairlist(1, letters)) alike(NULL, 1:3) # not a wild card at top level alike(list(NULL), list(1:3)) # but yes when nested alike(list(NULL, NULL), list(list(list(1, 2, 3)), 1:25)) alike(list(NULL), list(1, 2)) alike(list(), list(1, 2)) alike(matrix(integer(), ncol=7), matrix(1:21, nrow=3)) alike(matrix(character(), nrow=3), matrix(1:21, nrow=3)) alike( matrix(integer(), ncol=3, dimnames=list(NULL, c("R", "G", "B"))), matrix(1:21, ncol=3, dimnames=list(NULL, c("R", "G", "B"))) ) # Adding tests from docs mx.tpl <- matrix( integer(), ncol=3, dimnames=list(row.id=NULL, c("R", "G", "B")) ) mx.cur <- matrix( sample(0:255, 12), ncol=3, dimnames=list(row.id=1:4, rgb=c("R", "G", "B")) ) mx.cur2 <- matrix(sample(0:255, 12), ncol=3, dimnames=list(1:4, c("R", "G", "B"))) alike(mx.tpl, mx.cur2) La inspección en gdb muestra bastante rápido (si entiendo correctamente) que CSR_strmlen_x está tratando de acceder a la cadena que no se inicializó.
ACTUALIZACIÓN 2 : esta es una función altamente recursiva, y además de eso, el bit de inicialización de cadena se llama muchas, muchas veces. Esto se debe principalmente a que estaba siendo perezoso, solo necesitamos que las cadenas se inicialicen la única vez que encontramos algo que queremos informar en la recursión, pero fue más fácil inicializar cada vez que es posible encontrar algo. Menciono esto porque lo que verá a continuación muestra varias inicializaciones, pero solo se está utilizando una de ellas (presumiblemente la que tiene la dirección <0x1400000001>).
No puedo garantizar que lo que muestro aquí esté directamente relacionado con el elemento que causó la falla de segmento (aunque es el mismo acceso ilegal a la dirección), pero como @nate-eldredge preguntó, muestra que el elemento de la matriz no es inicializado justo antes del retorno o justo después del retorno en la función de llamada. Tenga en cuenta que la función de llamada está inicializando 8 de estos, y los muestro todos, con todos llenos de basura o memoria inaccesible.
ACTUALIZACIÓN 3 , desmontaje de la función en cuestión:
Breakpoint 1, ALIKEC_res_strings_init () at alike.c:75 75 return res; (gdb) p res.current[0] $1 = 0x7ffff46a0aa5 "%s%s%s%s" (gdb) p res.current[1] $2 = 0x1400000001 <error: Cannot access memory at address 0x1400000001> (gdb) disas /m ALIKEC_res_strings_init Dump of assembler code for function ALIKEC_res_strings_init: 53 struct ALIKEC_res_strings ALIKEC_res_strings_init() { 0x00007ffff4687fc0 <+0>: endbr64 54 struct ALIKEC_res_strings res; 55 56 res.target = (const char **) R_alloc(5, sizeof(const char *)); 0x00007ffff4687fc4 <+4>: push %r12 0x00007ffff4687fc6 <+6>: mov $0x8,%esi 0x00007ffff4687fcb <+11>: mov %rdi,%r12 0x00007ffff4687fce <+14>: push %rbx 0x00007ffff4687fcf <+15>: mov $0x5,%edi 0x00007ffff4687fd4 <+20>: sub $0x8,%rsp 0x00007ffff4687fd8 <+24>: callq 0x7ffff4687180 <R_alloc@plt> 0x00007ffff4687fdd <+29>: mov $0x8,%esi 0x00007ffff4687fe2 <+34>: mov $0x5,%edi 0x00007ffff4687fe7 <+39>: mov %rax,%rbx 57 res.current = (const char **) R_alloc(5, sizeof(const char *)); 0x00007ffff4687fea <+42>: callq 0x7ffff4687180 <R_alloc@plt> 58 59 res.target[0] = "%s%s%s%s"; 0x00007ffff4687fef <+47>: lea 0x1764a(%rip),%rdx # 0x7ffff469f640 0x00007ffff4687ff6 <+54>: lea 0x18aa8(%rip),%rcx # 0x7ffff46a0aa5 0x00007ffff4687ffd <+61>: mov %rcx,(%rbx) 60 res.target[1] = ""; 61 res.target[2] = ""; 0x00007ffff4688000 <+64>: mov %rdx,0x10(%rbx) 62 res.target[3] = ""; 0x00007ffff4688004 <+68>: mov %rdx,0x18(%rbx) 63 res.target[4] = ""; 0x00007ffff4688008 <+72>: mov %rdx,0x20(%rbx) 64 65 res.tar_pre = "be"; 66 67 res.current[0] = "%s%s%s%s"; 0x00007ffff468800c <+76>: mov %rax,0x8(%r12) 0x00007ffff4688011 <+81>: mov %rcx,(%rax) 68 res.current[1] = ""; 69 res.current[2] = ""; 0x00007ffff4688014 <+84>: mov %rdx,0x10(%rax) 70 res.current[3] = ""; 0x00007ffff4688018 <+88>: mov %rdx,0x18(%rax) 71 res.current[4] = ""; 0x00007ffff468801c <+92>: mov %rdx,0x20(%rax) 72 73 res.cur_pre = "is"; 74 75 return res; => 0x00007ffff4688020 <+96>: lea 0x14fe0(%rip),%rax # 0x7ffff469d007 0x00007ffff4688027 <+103>: mov %rax,0x10(%r12) 0x00007ffff468802c <+108>: lea 0x14fcd(%rip),%rax # 0x7ffff469d000 0x00007ffff4688033 <+115>: mov %rbx,(%r12) 0x00007ffff4688037 <+119>: mov %rax,0x18(%r12) 0x00007ffff468803c <+124>: add $0x8,%rsp 0x00007ffff4688040 <+128>: pop %rbx 0x00007ffff4688041 <+129>: mov %r12,%rax 0x00007ffff4688044 <+132>: pop %r12 0x00007ffff4688046 <+134>: retq 0x00007ffff4688047: nopw 0x0(%rax,%rax,1) End of assembler dump.ACTUALIZACIÓN 4 :
Entonces, tratando de analizar el estándar, aquí están las partes que parecen relevantes ( borrador C11 ):
6.3.2.3 Conversiones Par7 > Otros operandos > Punteros
Un puntero a un tipo de objeto puede convertirse en un puntero a un tipo de objeto diferente. Si el puntero resultante no está correctamente alineado 68) para el tipo referenciado, el comportamiento no está definido.
De lo contrario, cuando se vuelva a convertir, el resultado se comparará con el puntero original. Cuando un puntero a un objeto se convierte en un puntero a un tipo de carácter, el resultado apunta al byte direccionado más bajo del objeto. Los incrementos sucesivos del resultado, hasta el tamaño del objeto, generan punteros a los bytes restantes del objeto.
6.5 Expresiones Par6
El tipo efectivo de un objeto para acceder a su valor almacenado es el tipo declarado del objeto, si lo hay. 87) Si se almacena un valor en un objeto que no tiene un tipo declarado a través de un lvalue que tiene un tipo que no es un tipo de carácter, entonces el tipo del lvalue se convierte en el tipo efectivo del objeto para ese acceso y para accesos posteriores que no modificar el valor almacenado. Si un valor se copia en un objeto que no tiene un tipo declarado usando memcpy o memmove, o se copia como una matriz de tipo de carácter, entonces el tipo efectivo del objeto modificado para ese acceso y para los accesos posteriores que no modifican el valor es el tipo efectivo del objeto del que se copia el valor, si lo tiene. Para todos los demás accesos a un objeto que no tiene un tipo declarado, el tipo efectivo del objeto es simplemente el tipo del lvalue utilizado para el acceso.
87) Los objetos asignados no tienen tipo declarado.
IIUC R_alloc devuelve un desplazamiento en un bloque malloc ed que se garantiza que tiene double alineación, y el tamaño del bloque después del desplazamiento es del tamaño solicitado (también hay una asignación antes del desplazamiento para datos específicos de R). R_alloc lanza ese puntero a (char *) al regresar.
Sección 6.2.5 Párrafo 29
Un puntero a void tendrá los mismos requisitos de representación y alineación que un puntero a un tipo de carácter. 48) Del mismo modo, los punteros a versiones cualificadas o no cualificadas de tipos compatibles tendrán los mismos requisitos de representación y alineación. Todos los punteros a tipos de estructuras tendrán los mismos requisitos de representación y alineación que los demás.
Todos los punteros a tipos de unión tendrán los mismos requisitos de representación y alineación que los demás.
No es necesario que los punteros a otros tipos tengan los mismos requisitos de representación o alineación.48) Los mismos requisitos de representación y alineación están destinados a implicar intercambiabilidad como argumentos para funciones, valores de retorno de funciones y miembros de uniones.
Entonces, la pregunta es "¿podemos cambiar el (char *) a (const char **) y escribirlo como (const char **) ". Mi lectura de lo anterior es que siempre que los punteros en los sistemas en los que se ejecuta el código tengan una alineación compatible con double alineación, entonces está bien.
¿Estamos violando el "aliasing estricto"? es decir:
6.5 Par 7
Un objeto tendrá acceso a su valor almacenado solo mediante una expresión lvalue que tenga uno de los siguientes tipos: 88)
— un tipo compatible con el tipo efectivo del objeto...
88) La intención de esta lista es especificar aquellas circunstancias en las que un objeto puede o no ser alias.
Entonces, ¿qué debería pensar el compilador que es el tipo efectivo del objeto al que apunta res.target (o res.current )? Presumiblemente, el tipo declarado (const char **) , ¿o es realmente ambiguo? Me parece que no es en este caso solo porque no hay otro 'lvalue' en el alcance que acceda al mismo objeto.
Admito que estoy luchando mucho para extraer sentido de estas secciones de la norma.