Tengo un paquete R con código compilado en C que ha sido relativamente estable durante bastante tiempo y con frecuencia se prueba 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ó a segfaularse rápidamente a lo largo de estas líneas:
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
He podido reproducir el segfault de manera consistente mediante el uso del rocker/r-base
contenedor acoplable gcc-10.0.1
con un 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ó por debajo gcc-10.0.0
, pero no tengo los datos.
Ejecuté la gcc-10.0.1 -O2
versión gdb
y noté algo que me parece extraño:
Al recorrer la sección resaltada, parece que se omite la inicialización de los segundos elementos de las matrices ( R_alloc
es una envoltura alrededor de malloc
la basura que se recolecta cuando se regresa el control a R; la falla predeterminada ocurre antes de regresar a R). Más tarde, el programa se bloquea cuando se accede al elemento no inicializado (en la versión gcc.10.0.1 -O2).
Arreglé esto inicializando explícitamente el elemento en cuestión en todas partes del código que eventualmente 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 este, aunque esto sólo va a reproducir, siempre y cuando debian:testing
contenedor ventana acoplable tiene gcc-10
al gcc-10.0.1
. Además, no solo ejecute estos comandos si no confía en mí .
Lo sentimos, 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 de R, después de escribir run
para llegar gdb
a ejecutar 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 lo entiendo correctamente) que
CSR_strmlen_x
está intentando acceder a la cadena que no se inicializó.
ACTUALIZACIÓN 2 : esta es una función altamente recursiva, y además el bit de inicialización de cadena se llama muchas, muchas veces. Esto es principalmente b / c. Estaba siendo vago, solo necesitamos las cadenas inicializadas por la única vez que realmente encontramos algo que queremos informar en la recursión, pero fue más fácil de inicializar cada vez que es posible encontrar algo. Menciono esto porque lo que verá a continuación muestra múltiples inicializaciones, pero solo se está utilizando una de ellas (presumiblemente la que tiene la dirección <0x1400000001>).
No puedo garantizar que las cosas que muestro aquí estén directamente relacionadas con el elemento que causó la falla predeterminada (aunque es el mismo acceso de dirección ilegal), pero como preguntó @ nate-eldredge, muestra que el elemento de 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 a todos, con todos ellos 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, tratar de analizar el estándar aquí son 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á alineado correctamente 68) para el tipo referenciado, el comportamiento no está definido.
De lo contrario, cuando se convierta nuevamente, el resultado se comparará igual al 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, arrojan punteros a los bytes restantes del objeto.
6.5 Expresiones de Par6
El tipo efectivo de un objeto para acceder a su valor almacenado es el tipo declarado del objeto, si lo hay. 87) Si un valor se almacena en un objeto que no tiene un tipo declarado a través de un valor que tiene un tipo que no es un tipo de caracteres, entonces el tipo del valor se convierte en el tipo efectivo del objeto para ese acceso y para accesos posteriores que no Modificar el valor almacenado. Si se copia un valor en un objeto que no tiene un tipo declarado usando memcpy o memmove, o se copia como una matriz de tipo de caracteres, entonces el tipo efectivo del objeto modificado para ese acceso y para accesos posteriores que no modifican el valor es el tipo efectivo del objeto desde el que se copia el valor, si tiene uno. 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 valor l utilizado para el acceso.
87) Los objetos asignados no tienen tipo declarado.
IIUC R_alloc
devuelve un desplazamiento en un malloc
bloque ed que se garantiza que está double
alineado, 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 la (char *)
vuelta.
Sección 6.2.5 Par 29
Un puntero a anular 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 calificadas o no calificadas de tipos compatibles tendrán los mismos requisitos de representación y alineación. Todos los punteros a los tipos de estructura deben tener 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.
Los punteros a otros tipos no necesitan tener 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 a funciones, valores de retorno de funciones y miembros de sindicatos.
Por lo tanto, la pregunta es "¿se nos permite refundir el (char *)
to (const char **)
y escribirle como (const char **)
". Mi lectura de lo anterior es que mientras los punteros en los sistemas en los que se ejecuta el código tengan una alineación compatible con la double
alineación, entonces está bien.
¿Estamos violando el "alias 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 tener un alias o no.
Entonces, ¿qué debería pensar el compilador que es el tipo efectivo del objeto señalado por 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 poderosamente para extraer sentido de estas secciones de la norma.
-mtune=native
optimiza para la CPU particular que tiene su máquina. Eso será diferente para diferentes evaluadores y puede ser parte del problema. Si ejecuta la compilación -v
, debería poder ver qué familia de CPU está en su máquina (por ejemplo, -mtune=skylake
en mi computadora).
disassemble
instrucciones dentro de gdb.