Eu tenho um pacote R com código compilado C que é relativamente estável por um bom tempo e é frequentemente testado em uma ampla variedade de plataformas e compiladores (windows / osx / debian / fedora gcc / clang).
Mais recentemente, uma nova plataforma foi adicionada para testar o pacote novamente:
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"
Nesse ponto, o código compilado imediatamente iniciou o segfaulting ao longo destas linhas:
*** caught segfault ***
address 0x1d00000001, cause 'memory not mapped'
Consegui reproduzir o segfault de forma consistente usando o rocker/r-base
contêiner do docker gcc-10.0.1
com nível de otimização -O2
. A execução de uma otimização mais baixa elimina o problema. A execução de qualquer outra configuração, inclusive sob valgrind (-O0 e -O2), UBSAN (gcc / clang), não mostra nenhum problema. Também tenho certeza de que isso ocorreu gcc-10.0.0
, mas não os dados.
Corri a gcc-10.0.1 -O2
versão gdb
e notei algo que me parece estranho:
Ao percorrer a seção destacada, parece que a inicialização dos segundos elementos das matrizes é ignorada ( R_alloc
é um invólucro em torno do malloc
qual o lixo próprio é coletado ao retornar o controle para R; o segfault ocorre antes de retornar para R). Posteriormente, o programa falha quando o elemento não inicializado (na versão gcc.10.0.1 -O2) é acessado.
Corrigi isso inicializando explicitamente o elemento em questão em qualquer lugar do código que eventualmente levou ao uso do elemento, mas ele realmente deveria ter sido inicializado com uma string vazia, ou pelo menos é o que eu teria assumido.
Estou perdendo algo óbvio ou fazendo algo estúpido? Ambos são razoavelmente prováveis, pois C é minha segunda língua de longe . É estranho que isso tenha surgido agora e não consigo descobrir o que o compilador está tentando fazer.
UPDATE : Instruções para reproduzir isso, embora isso só seja reproduzido enquanto o debian:testing
contêiner estiver gcc-10
em gcc-10.0.1
. Além disso, não execute esses comandos apenas se não confiar em mim .
Desculpe, este não é um exemplo mínimo reproduzível.
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
Em seguida, no console R, após a digitação run
de obter gdb
para executar o 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)
A inspeção no gdb mostra rapidamente (se bem entendi) que
CSR_strmlen_x
está tentando acessar a string que não foi inicializada.
ATUALIZAÇÃO 2 : esta é uma função altamente recursiva e, além disso, o bit de inicialização da string é chamado muitas e muitas vezes. Isso é principalmente porque eu estava sendo preguiçoso, precisamos apenas das seqüências inicializadas pela única vez que encontramos algo que queremos relatar na recursão, mas era mais fácil inicializar toda vez que é possível encontrar algo. Menciono isso porque o que você verá a seguir mostra várias inicializações, mas apenas uma delas (presumivelmente a com endereço <0x1400000001>) está sendo usada.
Não posso garantir que as coisas que estou mostrando aqui estejam diretamente relacionadas ao elemento que causou o segfault (embora seja o mesmo acesso ilegal ao endereço), mas como @ nate-eldredge pediu, ele mostra que o elemento array não é inicializado imediatamente antes do retorno ou logo após o retorno na função de chamada. Observe que a função de chamada está inicializando 8 deles, e eu mostro todos eles, com todos eles cheios de lixo ou memória inacessível.
ATUALIZAÇÃO 3 , desmontagem da função em questão:
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.
ATUALIZAÇÃO 4 :
Então, tentando analisar o padrão aqui estão as partes dele que parecem relevantes ( rascunho C11 ):
6.3.2.3 Conversões Par7> Outros operandos> Ponteiros
Um ponteiro para um tipo de objeto pode ser convertido em um ponteiro para um tipo de objeto diferente. Se o ponteiro resultante não estiver alinhado corretamente 68) para o tipo referenciado, o comportamento será indefinido.
Caso contrário, quando convertido novamente, o resultado será comparado ao ponteiro original. Quando um ponteiro para um objeto é convertido em um ponteiro para um tipo de caractere, o resultado aponta para o byte endereçado mais baixo do objeto. Incrementos sucessivos do resultado, até o tamanho do objeto, produzem ponteiros para os bytes restantes do objeto.
6.5 Expressões Par6
O tipo efetivo de um objeto para um acesso ao seu valor armazenado é o tipo declarado do objeto, se houver. 87) Se um valor for armazenado em um objeto sem tipo declarado por meio de um lvalue com um tipo que não seja um tipo de caractere, o tipo do lvalue se tornará o tipo efetivo do objeto para esse acesso e para acessos subseqüentes que não modificar o valor armazenado. Se um valor for copiado em um objeto sem tipo declarado usando memcpy ou memmove ou for copiado como uma matriz de tipo de caractere, o tipo efetivo do objeto modificado para esse acesso e para acessos subseqüentes que não modificam o valor é o tipo efetivo do objeto do qual o valor é copiado, se houver um. Para todos os outros acessos a um objeto sem tipo declarado, o tipo efetivo do objeto é simplesmente o tipo de lvalue usado para o acesso.
87) Objetos alocados não têm tipo declarado.
IIUC R_alloc
retorna um deslocamento em um malloc
bloco ed que é garantido para ser double
alinhado, e o tamanho do bloco após o deslocamento é do tamanho solicitado (também há alocação antes do deslocamento para dados específicos de R). R_alloc
lança esse ponteiro (char *)
no retorno.
Seção 6.2.5 Par 29
Um ponteiro para anular deve ter os mesmos requisitos de representação e alinhamento que um ponteiro para um tipo de caractere. 48) Da mesma forma, os indicadores para versões qualificadas ou não qualificadas de tipos compatíveis devem ter os mesmos requisitos de representação e alinhamento. Todos os ponteiros para tipos de estrutura devem ter os mesmos requisitos de representação e alinhamento que os outros.
Todos os ponteiros para tipos de união devem ter os mesmos requisitos de representação e alinhamento que os outros.
Ponteiros para outros tipos não precisam ter os mesmos requisitos de representação ou alinhamento.48) Os mesmos requisitos de representação e alinhamento significam implicar argumentos de intercambiabilidade para funções, retornar valores de funções e membros de sindicatos.
Portanto, a questão é "podemos reformular o (char *)
to (const char **)
e escrever nele como (const char **)
". Minha leitura do exposto acima é que, enquanto os ponteiros nos sistemas em que o código é executado tiverem alinhamento compatível com o double
alinhamento, tudo bem.
Estamos violando o "aliasing estrito"? ou seja:
6.5 Par 7
Um objeto deve ter seu valor armazenado acessado apenas por uma expressão lvalue que possui um dos seguintes tipos: 88)
- um tipo compatível com o tipo efetivo do objeto ...
88) O objetivo desta lista é especificar as circunstâncias nas quais um objeto pode ou não ser aliasado.
Então, o que o compilador deve pensar que é o tipo efetivo do objeto apontado por res.target
(ou res.current
)? Presumivelmente, o tipo declarado (const char **)
, ou isso é realmente ambíguo? Parece-me que não é neste caso apenas porque não há outro 'lvalue' no escopo que acesse o mesmo objeto.
Admito que estou lutando poderosamente para extrair sentido dessas seções do padrão.
-mtune=native
otimiza para a CPU específica que sua máquina possui. Isso será diferente para testadores diferentes e pode fazer parte do problema. Se você executar a compilação, -v
poderá ver qual família de CPUs está na sua máquina (por exemplo, -mtune=skylake
no meu computador).
disassemble
instruções dentro do gdb.