Sopa de opções: as armadilhas sutis de combinar sinalizadores de compilador – Mozilla Hacks

PUBLICIDADE

Sopa de opções: as armadilhas sutis de combinar sinalizadores de compilador - Mozilla Hacks

O desenvolvimento do Firefox revela muitas diferenças entre plataformas e recursos exclusivos de sua combinação de dependências. Os engenheiros que trabalham no Firefox superam regularmente esses desafios e, embora não possamos detalhar todos eles, achamos que você gostará de ouvir sobre alguns, então aqui está um exemplo de uma investigação técnica recente.

Durante o ciclo beta do Firefox 120, uma nova assinatura de travamento apareceu em nossos radares com volume significativo.

Naquela época, a distribuição entre sistemas operacionais revelou que mais de 50% do volume de travamentos se originava de usuários do Ubuntu 18.04 LTS.

O processo principal falha em um CanvasRenderer thread, com a seguinte pilha de chamadas:

0  firefox  std::locale::operator=  
1  firefox  std::ios_base::imbue  
2  firefox  std::basic_ios >::imbue  
3  libxul.so  sh::InitializeStream<:__cxx11::basic_ostringstream std::char_traits="">, std::allocator > >  /build/firefox-ZwAdKm/firefox-120.0~b2+build1/gfx/angle/checkout/src/compiler/translator/Common.h:238
3  libxul.so  sh::TCompiler::setResourceString  /build/firefox-ZwAdKm/firefox-120.0~b2+build1/gfx/angle/checkout/src/compiler/translator/Compiler.cpp:1294
4  libxul.so  sh::TCompiler::Init  /build/firefox-ZwAdKm/firefox-120.0~b2+build1/gfx/angle/checkout/src/compiler/translator/Compiler.cpp:407
5  libxul.so  sh::ConstructCompiler  /build/firefox-ZwAdKm/firefox-120.0~b2+build1/gfx/angle/checkout/src/compiler/translator/ShaderLang.cpp:368
6  libxul.so  mozilla::webgl::ShaderValidator::Create  /build/firefox-ZwAdKm/firefox-120.0~b2+build1/dom/canvas/WebGLShaderValidator.cpp:215
6  libxul.so  mozilla::WebGLContext::CreateShaderValidator const  /build/firefox-ZwAdKm/firefox-120.0~b2+build1/dom/canvas/WebGLShaderValidator.cpp:196
7  libxul.so  mozilla::WebGLShader::CompileShader  /build/firefox-ZwAdKm/firefox-120.0~b2+build1/dom/canvas/WebGLShader.cpp:98

À primeira vista, queremos culpar o WebGL. As funções da biblioteca padrão C++ não podem ser culpadas, certo?

Mas ao olhar para o código WebGL, a falha ocorre nas linhas perfeitamente válidas de C++ resumidas abaixo:

std::ostringstream stream;
stream.imbue(std::locale::classic());

Este código nunca deveria travar, mas trava. Na verdade, olhar mais de perto a pilha fornece uma primeira pista para investigação:
Embora nos deparemos com funções que pertencem à biblioteca padrão C++, essas funções parecem residir no binário do Firefox.

Esta é uma situação incomum que nunca ocorre com versões oficiais do Firefox.
No entanto, é muito comum que a distribuição altere as definições de configuração e aplique patches downstream a uma fonte upstream, não se preocupe com isso.
Além disso, existe apenas uma única versão do Firefox Beta que está causando esse travamento.

Sabemos disso graças a um identificador exclusivo associado a qualquer binário ELF.
Aqui, se escolhermos qualquer versão específica do Firefox 120 Beta (como 120b9), todas as falhas incorporam o mesmo identificador exclusivo para o Firefox.

Agora, como podemos adivinhar qual compilação produz esse binário estranho?

Um comentário útil do usuário menciona que eles enfrentam essa falha regularmente desde a atualização para 120.0~b2+build1-0ubuntu0.18.04.1.
E ao procurar esse identificador de compilação, chegamos rapidamente ao Firefox Beta PPA.
Então, de fato, podemos reproduzir o travamento instalando-o em uma máquina virtual Ubuntu 18.04 LTS: ele ocorre ao carregar qualquer página WebGL!
Com o binário agora em mãos, executando nm -D ./firefox confirma a presença de vários símbolos relacionados ao libstdc++ que residem na seção de texto (T marcador).

Símbolos modelados e embutidos da libstdc++ geralmente aparecem como fracos (W marcador), então há apenas uma explicação para esta situação: o firefox foi vinculado estaticamente ao libstdc++, provavelmente através -static-libstdc++.

Felizmente, os logs de construção estão disponíveis para todos os pacotes do Ubuntu.
Depois de algumas pesquisas, encontramos os logs da compilação 120b9, que de fato contém referências a -static-libstdc++.

Mas por que?

Novamente, tudo está bem documentado e, graças a habilidades de escavação bem treinadas, chegamos a um relatório de erros que fornece informações interessantes.
O Firefox requer um compilador C++ moderno e, portanto, um libstdc++ moderno, que não está disponível em sistemas antigos como o Ubuntu 18.04 LTS.
A construção usa -static-libstdc++ para fechar essa lacuna.
Isso apenas explica a configuração estranha.

E o acidente?

Como agora podemos reproduzi-lo, podemos iniciar o Firefox em um depurador e continuar nossa investigação.
Ao inspecionar o local do acidente, parecemos travar porque std::locale::classic() não foi inicializado corretamente.
Vamos dar uma olhada na implementação.

const locale& locale::classic()
{
  _S_initialize();
  return *(const locale*)c_locale;
}

_S_initialize() é responsável por garantir que c_locale será inicializado corretamente antes de retornarmos uma referência a ele.
Para conseguir isso, _S_initialize() chama outra função, _S_initialize_once().

void locale::_S_initialize()
{
#ifdef __GTHREADS
  if (!__gnu_cxx::__is_single_threaded())
    __gthread_once(&_S_once, _S_initialize_once);
#endif

  if (__builtin_expect(!_S_classic, 0))
    _S_initialize_once();
}

Em _S_initialize()primeiro passamos por um wrapper para pthread_once(): o primeiro thread que atinge este código consome _S_once e chamadas _S_initialize_once()enquanto outros threads (se houver) ficam presos aguardando _S_initialize_once() para completar.

Isso parece bastante à prova de falhas, certo?

Existe ainda uma chamada direta extra para _S_initialize_once() se _S_classic ainda não foi inicializado depois disso.
Agora, _S_initialize_once() em si é bastante simples: ele aloca _S_classic e coloca dentro c_locale.

void
locale::_S_initialize_once() throw()
{
  // Need to check this because we could get called once from _S_initialize()
  // when the program is single-threaded, and then again (via __gthread_once)
  // when it's multi-threaded.
  if (_S_classic)
    return;

  // 2 references.
  // One reference for _S_classic, one for _S_global
  _S_classic = new (&c_locale_impl) _Impl(2);
  _S_global = _S_classic;
  new (&c_locale) locale(_S_classic);
}

O acidente parece que nunca passamos _S_initialize_once()então vamos colocar um ponto de interrupção lá e ver o que acontece.
E só de fazer isso já notamos algo suspeito.
Nós alcançamos _S_initialize_once()mas não dentro do binário do Firefox: em vez disso, alcançamos apenas a versão exportada por liblgpllibs.so.
Na verdade, liblgpllibs.so também está estaticamente vinculado ao libstdc++, de modo que o firefox e o liblgpllibs.so incorporam e exportam seus próprios _S_initialize_once() função.

Por padrão, interposição de símbolos se aplica, e _S_initialize_once() deve sempre ser chamado através da tabela de ligação de procedimentos (PLT), para que cada módulo acabe chamando a mesma versão da função.
Se a interposição de símbolos estivesse acontecendo aqui, esperaríamos que liblgpllibs.so alcançasse a versão de _S_initialize_once() exportada pelo firefox em vez da sua própria, porque o firefox foi carregado primeiro.

Então talvez não haja interposição de símbolos.

Isto pode ocorrer ao usar -fno-semantic-interposition.

Cada versão da biblioteca padrão existiria por conta própria, independente das outras versões.
Mas nem o sistema de compilação do Firefox nem o mantenedor do Ubuntu parecem passar esse sinalizador para o compilador.
No entanto, olhando para a desmontagem para _S_initialize() e _S_initialize_once()podemos ver que as variáveis ​​globais exportadas (_S_once, _S_classic, _S_global) são sujeito a interposição de símbolos:

Todos esses acessos passam pela tabela de deslocamento global (GOT), de forma que cada módulo acabe acessando a mesma versão da variável.
Isso parece estranho, dado o que dissemos anteriormente sobre _S_initialize_once().
Variáveis ​​globais não exportadas (c_locale, c_locale_impl), entretanto, são acessados ​​diretamente sem interposição de símbolos, como esperado.

Agora temos informações suficientes para explicar o acidente.

Quando chegarmos _S_initialize() em liblgpllibs.so, na verdade consumimos o _S_once que vive no firefox e inicializa o _S_classic e _S_global que vivem no Firefox.
Mas nós os inicializamos com ponteiros para variáveis ​​bem inicializadas c_locale_impl e c_locale que vivem em liblgpllibs.so!
As variáveis c_locale_impl e c_locale que vivem no Firefox, no entanto, permanecem não inicializados.

Então, se mais tarde chegarmos _S_initialize() no firefox, tudo parece como se a inicialização tivesse acontecido.
Mas então retornamos uma referência à versão de c_locale que reside no Firefox, e esta versão nunca foi inicializada.

Bum!

Agora a questão principal é: por que vemos a interposição ocorrer para _S_once mas não para _S_initialize_once()?
Se recuarmos por um minuto, há uma distinção fundamental entre estes símbolos: um é um símbolo de função, o outro é um símbolo de variável.
E, de fato, o sistema de compilação do Firefox usa o -Bsymbolic-function bandeira!

A página do manual ld descreve-o da seguinte forma:

-Bsymbolic-functions

When creating a shared library, bind references to global function symbols to the definition within the shared library, if any.  This option is only meaningful on ELF platforms which support shared libraries.

Ao contrário de:

-Bsymbolic

When creating a shared library, bind references to global symbols to the definition within the shared library, if any.  Normally, it is possible for a program linked against a shared library to override the definition within the shared library. This option is only meaningful on ELF platforms which support shared libraries.

Acertou em cheio!

A falha ocorre porque esse sinalizador nos faz usar uma variante estranha de interposição de símbolos, onde a interposição de símbolos acontece para símbolos variáveis ​​como _S_once e _S_classic mas não para símbolos de função como _S_initialize_once().

Isto resulta numa incompatibilidade relativamente à forma como acedemos às variáveis ​​globais: as variáveis ​​globais exportadas são únicas graças à interposição, enquanto cada função não interposta irá aceder à sua própria versão de qualquer variável global não exportada.

Com todo o conhecimento que reunimos agora, é fácil escrever um reprodutor que não envolva nenhum código do Firefox:

/* main.cc */
#include 
extern void pain();
int main() {
pain();
   std::cout << "[main] " << std::locale::classic().name() <<"\n";
   return 0;
}

/* pain.cc */

#include 
void pain() {
std::cout << "[pain] " << std::locale::classic().name() <<"\n";
}

# Makefile
all:
   $(CXX) pain.cc -fPIC -shared -o libpain.so -static-libstdc++ -Wl,-Bsymbolic-functions
   $(CXX) main.cc -fPIC -c -o main.o
   $(CC) main.o -fPIC -o main /usr/lib/gcc/x86_64-redhat-linux/13/libstdc++.a -L. -Wl,-rpath=. -lpain -Wl,-Bsymbolic-functions
   ./main

clean:
   $(RM) libpain.so main

Compreender o bug é um passo e resolvê-lo é outra história.
Deve ser considerado um bug da libstdc++ o fato de o código para localidades não ser compatível com -static-stdlibc++ -Bsymbolic-functions?

Parece que combinar essas flags é uma ótima maneira de cavar nossa própria cova, e essa parece ser a opinião dos mantenedores da libstdc++.

No geral, talvez a parte mais estranha desta história é que esta combinação não causou nenhum problema até agora.
Portanto, sugerimos ao mantenedor do pacote parar de usar -static-libstdc++.

Existem outras maneiras de usar uma libstdc++ diferente da disponível no sistema, como usar vinculação dinâmica e definir um RPATH para vincular a uma versão empacotada.

Isso permitiu que eles implantassem com êxito uma versão fixa do pacote.
Poucos dias depois, com o lançamento oficial do Firefox 120, notamos um aumento muito significativo no volume para a mesma assinatura de travamento. De novo não!

Desta vez o volume veio exclusivamente de usuários do NixOS 23.05, e foi enorme!

Depois de compartilharmos as conclusões de nossa investigação beta com eles, os mantenedores do NixOS conseguiram associar rapidamente a falha a um problema que ainda não havia sido portado para 23.05 e estava fazendo com que o compilador se comportasse como -static-libstdc++.

Para evitar tal confusão no futuro, adicionamos detecção para esta configuração específica no arquivo configure do Firefox.

Estamos gratos às pessoas que ajudaram a resolver este problema, em particular:

  • Rico Tzschichholz (ricotz) que rapidamente corrigiu o pacote Ubuntu 18.04 LTS, e Amin Bandali (bandali) que forneceu ajuda no caminho;
  • Martin Weinelt (hexa) e Artturin por suas correções imediatas para o pacote NixOS 23.05;
  • Nicolas B. Pierron (nbp) por nos ajudar a começar a usar o NixOS, o que nos permitiu compartilhar rapidamente informações úteis com os mantenedores do pacote NixOS.

Mais artigos de Serge Guelton…

Mais artigos de Yannis Juglaret…

Fonte: Tecmundo, Olhar Digital, MeioBit

Mais recentes

PUBLICIDADE

WP Twitter Auto Publish Powered By : XYZScripts.com