Aaron Patterson no Rails World 2026: a regra "as-if", otimizações invisíveis e por que a IA não é o seu compilador
Ruby on RailsNo keynote de encerramento do Rails World 2026, Aaron Patterson (Tenderlove, integrante das equipes principais do Ruby e do Rails e da equipe de infraestrutura de Ruby on Rails da Shopify) parte de uma frase conhecida, "o propósito de um sistema é o que ele faz", e propõe uma correção: o propósito de um sistema é o que ele faz para quem o observa. A partir dessa ideia, ele examina otimizações de desempenho em três camadas: o código que os desenvolvedores escrevem, com memoização e Ractors; o interpretador CRuby, que remove quadros de pilha; e o compilador JIT ZJIT, que elimina alocações por meio de interpretação abstrata. Em cada caso, a pergunta é quem consegue perceber a mudança. Na parte final, que ele mesmo chamou de "segunda apresentação", Patterson conta como agentes de IA encadearam um bug de cache do RubyGems.org e o rubydoc.info em um ataque. Sua conclusão é que um compilador promete preservar o comportamento observável do seu programa e a IA não faz essa promessa.
Abertura: piadas sobre IA e um anúncio sério
Patterson abriu com uma sequência de piadas sobre o clima de "AI World" da conferência. Disse que tinha criado a própria distribuição Linux, "AaronXP", "codificada na vibração", com instalação de 100 milissegundos, porque reinstala o sistema operacional o tempo todo. Comentou que achava irônico fazer piada sobre a crise financeira e, em seguida, incentivar todo mundo a gerar montes de código em Rust sem lê-lo, e ofereceu vender "seguro contra dívida técnica". Também brincou que a IA transforma todos em "programadores 1.000x", corrigindo em seguida o suposto erro de digitação para "1.000 ex-programadores". Ele deixou claro, porém, que também adora usar IA.
O momento sério da abertura foi o anúncio de que Mike Dalessio é o mais novo membro da equipe principal do Rails. Patterson contou que os dois se conhecem há mais de 20 anos, desde que Dalessio enviou um pull request para o Mechanize, uma biblioteca Ruby da época. Desde então trabalharam juntos em código aberto, e Dalessio chegou a ser chefe de Patterson na Shopify. Os dois criaram a organização "Sparkle Motion" no GitHub. Segundo Patterson, ele é o "Sparkle", que sobe ao palco com gestos teatrais, e Dalessio é o "Motion", que faz o trabalho de verdade. Entre os projetos em comum estão o Nokogiri e a gem SQLite3, e Dalessio é muito ativo na equipe de segurança do Rails.
Patterson apresentou os temas do ano: desempenho, Ractors, ZJIT e IA. Mencionou também que escreveu um alocador de registradores para o ZJIT. Não falaria dele na palestra, mas quis registrar que foi o projeto mais difícil em que já trabalhou e que está muito orgulhoso do resultado.
"O propósito de um sistema é o que ele faz", para quem observa
Patterson contou que nunca tinha pensado muito na frase "o propósito de um sistema é o que ele faz" e que ela o incomoda por ser ambígua: se não sabe o que o sistema faz, também não consegue dizer qual é o propósito. Para ilustrar, imaginou viajar no tempo com um Nokia 3310, o celular "indestrutível". Alguém que nunca viu um celular poderia usá-lo como martelo. Lida literalmente, a frase diria que o propósito do aparelho é ser um martelo. Para Patterson, continua sendo um celular. O que o sistema "faz" depende de quem olha para ele.
Por isso ele reformulou a citação: o propósito de um sistema é o que ele faz para aqueles que o observam. Segundo Patterson, essa atitude observacional é bem conhecida entre implementadores de linguagens com o nome de regra "as-if" ("como se"). No C++ ela está codificada: o compilador pode aplicar qualquer transformação de otimização desde que ela não altere o comportamento observável do programa conforme especificado pela norma. Em outras palavras, o compilador pode mudar o código como quiser, contanto que a execução pareça seguir o que foi escrito.
Patterson ressalvou que a expressão "conforme especificado pela norma" é estranha no contexto de Ruby, que não tem um padrão como o do C++, mas deixou essa questão de lado. A proposta da palestra é olhar otimizações pelo ângulo do comportamento observável. Toda otimização tem dois lados: o que ela afeta e quem a observa.
Memoização e o novo observador trazido pelos Ractors
O primeiro exemplo é um padrão que, segundo Patterson, praticamente todo mundo já escreveu: memoização. Há uma função cara que só precisa ser calculada uma vez, e o resultado fica guardado em uma variável de instância. O problema é a suposição embutida de que apenas uma thread executa aquele código por vez. Existe uma condição de corrida que pode fazer o cálculo caro rodar várias vezes, e ninguém percebe porque o Ruby tem a GVL.
Os Ractors permitem paralelismo real em Ruby, e com isso os observadores do código mudam: duas threads podem de fato percorrer o mesmo caminho ao mesmo tempo. Patterson destacou que, nesse caso, os Ractors lançam uma exceção. Em vez de uma condição de corrida silenciosa que quebra o aplicativo, o desenvolvedor recebe um erro e tem a chance de corrigir o problema. Ele remeteu à palestra de Andrew no mesmo evento, que tratou disso em detalhe.
Patterson insistiu que isso não significa abandonar o padrão e mostrou dois casos. No primeiro, vários Ractors acessam a variável de instância memoizada no nível da classe Foo. Há condição de corrida, e o Ractor R1 lança uma exceção avisando que aquilo precisa ser feito de forma segura para threads. No segundo, o padrão é o mesmo, mas existe apenas um observador daquele caminho de código, e aí está tudo certo. Basta garantir que o estado fique isolado em um Ractor específico.
Como variáveis de instância no nível da classe são comuns em muitas bases de código, a próxima versão do Rails terá uma forma de lidar com elas: o módulo ActiveSupport::Ractors, com um método on_main que executa determinado código no Ractor principal. Um cálculo demorado cujo resultado precisa ficar numa variável de classe pode rodar ali. Patterson alertou, retomando a palestra de Andrew, que isso gera latência: se muito trabalho for transferido para o Ractor principal, ele vira o gargalo da aplicação e todos passam a disputá-lo.
Sempre que possível, ele recomendou a gem de estruturas de dados compatíveis com Ractor escrita por Koichi Sasada, autor principal dos Ractors (citada na palestra como "Ractor sharing"). Patterson se corrigiu no palco: não "espera" que funcione bem, "vai funcionar". Ele destacou duas estruturas dessa gem que resolvem o problema da memoização, e a escolha entre elas depende da vazão desejada:
- Lock variables (variáveis de bloqueio) garantem que o cálculo ocorra exatamente uma vez. Servem para cálculos não idempotentes.
- TVars permitem que o cálculo ocorra uma ou algumas vezes. Servem quando o cálculo é idempotente e repeti-lo ocasionalmente é aceitável.
O alcance limitado dos problemas com Ractors
Patterson disse que não queria assustar ninguém. Pela forma como os Ractors são implementados, ele argumenta, não haverá deadlocks nem problemas de segurança de threads, e sim exceções. Além disso, acredita que o alcance desses problemas seja bastante limitado.
Para sustentar isso, mostrou um servidor web simulado baseado em Ractors: um laço infinito lê requisições do socket e atende cada uma dentro de um Ractor. No ciclo normal de requisição e resposta, a maioria dos objetos é alocada dentro do método serve. O desenvolvedor só precisa se preocupar quando o código acessa algo externo, como constantes ou valores de configuração. Na avaliação de Patterson, o número desses pontos de contato é bem menor do que as pessoas imaginam.
GC local por Ractor no Ruby 4.1 e os objetos compartilhados implicitamente
O tema seguinte é como a observabilidade de dados afeta o desempenho. Patterson explicou que o coletor de lixo do Ruby é um singleton: todo Ractor que precisa alocar um objeto pede ao GC, e com vários Ractors em paralelo isso vira um gargalo. O Ruby 4.1 será lançado com coleta de lixo local por Ractor, com heaps separados para cada um, o que elimina esse gargalo.
Ele levantou uma possibilidade teórica: no servidor simulado, criar um Ractor novo a cada requisição e simplesmente descartar o heap inteiro ao final, o que daria coletas mais eficientes. Patterson disse que gostaria que a história terminasse ali, mas ela é mais complicada. Num exemplo de produtor e consumidor, o produtor aloca um objeto no próprio heap e o passa ao consumidor. O object_id mostra que os dois apontam exatamente para o mesmo objeto. Quando um desses Ractors termina, não dá para descartar seu heap, porque outros Ractors ainda podem apontar para ele. A solução é um GC global, que Patterson comparou a uma coleta "major" que, idealmente, ocorre com menos frequência.
O segundo exemplo, que ele disse já ter mostrado no Rails World do ano anterior, envolve compartilhamento implícito. Ao analisar dois documentos JSON, as chaves resultantes são o mesmo objeto, com o mesmo object_id. Se o código for dividido em vários Ractors, a string "hello" continua tendo o mesmo ID em todos eles, porque as chaves são desduplicadas entre Ractors. Na prática, a string é alocada no heap do Ractor principal e os outros apontam para ela. O ponto de Patterson é que isso pode afetar o desempenho do GC sem que o desenvolvedor saiba, porque acontece de forma implícita.
Callbacks do ActiveSupport: mais quadros de pilha do que o esperado
Depois das otimizações feitas por desenvolvedores, Patterson passou às do framework e da linguagem, a começar por inconsistências nos backtraces. Ele montou um "ActiveRecord simulado" com os callbacks do ActiveSupport: um módulo com um método save que executa callbacks de salvamento e imprime a pilha de chamadas de dentro do callback. A expectativa é ver o bloco de run_callbacks, depois o quadro de save e, no topo, main.
Sem callbacks, há de fato três quadros. Com um callback before, continuam três. Ao definir um callback around, o rastreamento de pilha cresce muito, embora nada dentro de save tenha mudado. Investigando, Patterson encontrou um comentário no ActiveSupport que ele achou engraçado. Diz que, como o método é usado em muitos lugares e costuma envolver grandes trechos de código do usuário, ele tem o objetivo adicional de minimizar seu impacto na pilha visível: uma exceção num callback before ou after pode fazer todo o ruído que quiser, mas quando o controle passa para o bloco fornecido, o código deve deixar o mínimo possível de indícios de que esteve ali. Para Patterson, é exatamente a regra "as-if". Ele disse perdoar o caso do around, porque o código da aplicação passou a fazer mais trabalho, e então é natural que apareçam mais quadros.
O Class#new que sumiu no Ruby 4.0
A pergunta seguinte foi se é possível ver menos quadros do que o esperado. Patterson brincou que gosta mesmo de passar os fins de semana contando quadros de pilha e que não acha que isso o torne um perdedor.
No exemplo, o método 1 chama o 2, que chama o 3, que instancia Foo, cujo initialize chama o método 4. A pilha esperada é initialize, new, 3, 2, 1, main, e é o que aparece no Ruby 3.4. No Ruby 4.0, o quadro de Class#new desaparece.
Para explicar, Patterson comparou as instruções da VM geradas para o método 3 nas duas versões. O Ruby 4.0 gera muito mais instruções porque a chamada a initialize foi colocada inline no local da chamada a new. Em pseudocódigo Ruby, a lógica é: se o new de Foo for a implementação padrão, aloca-se o objeto, chama-se initialize diretamente e devolve-se o objeto; se não for, segue-se um caminho lento que chama new. Como agora é o método 3 que chama initialize diretamente, o quadro de new some.
Ele demonstrou o caminho lento com um "monkey patch" simples de new, que apenas chama super. Antes do patch, o backtrace vem pelo caminho rápido, sem new. Depois dele, o quadro de new volta a aparecer.
O motivo de todo esse trabalho, segundo Patterson, é que as alocações ficam mais rápidas. No exemplo mostrado, a alocação no Ruby 4.0 é 70% mais rápida do que na versão anterior, e isso sem compilador JIT. O custo foi um quadro de chamada, e os afetados são quem inspeciona a pilha: o próprio chamador, depuradores e ferramentas semelhantes. Patterson considera a troca boa.
Ele disse que o CRuby faz isso o tempo todo e deu mais um exemplo. Uma classe Foo implementa hash e imprime a pilha sempre que o método é chamado. O código dispara o cálculo de hash de três maneiras. Seria de esperar que as duas primeiras produzissem a mesma pilha e a terceira uma pilha diferente, mas com a mesma profundidade. Na prática as pilhas são completamente diferentes, e em uma delas o quadro de um método chamador simplesmente não aparece.
Otimizações de baixo risco: as áreas cinzentas da regra
Patterson perguntou à plateia se alguém se importa com esses quadros sumidos. Quase ninguém levantou a mão, e ele disse que era exatamente essa a reação que queria: se as pessoas se importassem, recursos assim não poderiam ser implementados. Ele descreve esses casos como áreas cinzentas da regra "as-if". Estritamente falando, a execução mudou e dá para perceber a diferença, mas as pessoas não se importam. Em Ruby, e segundo ele na maioria das linguagens, é preciso preservar o comportamento onde ele realmente importa. Patterson chama essas mudanças de otimizações de baixo risco: alteram o comportamento da linguagem em pontos com que, em geral, ninguém se preocupa.
Dentro do ZJIT: apostas de alto risco com rede de segurança
Em seguida vieram as otimizações de alto risco dentro do ZJIT. Segundo Patterson, o ZJIT pode fazê-las porque consegue desfazer uma otimização quando algo inesperado acontece.
O ZJIT tem duas representações intermediárias: uma de alto nível (HIR) e uma de baixo nível (LIR). Quem quiser examinar o HIR pode passar ao Ruby a opção de dump do HIR do ZJIT. Uma das suposições do ZJIT é que o programa provavelmente não vai redefinir métodos. Se redefinir, o compilador precisa saber, para continuar se comportando corretamente.
No exemplo, um ponto é alocado e um método chamado dentro de um bloco 5.times invoca métodos desse ponto. No HIR, Patterson destacou três coisas: o bloco compilado; o método get colocado inline dentro do bloco, com a inserção de um quadro inline; e a constante 42 levada para dentro do bloco. O ZJIT transformou o código original em uma versão bem mais simples.
A aposta é ousada porque, como ele observou, ninguém se importa com um quadro de pilha a menos, mas se o código redefinir get ou x, a versão compilada deixaria de se comportar como esperado e violaria a regra "as-if". O HIR contém "patch points", um para a redefinição de get e outro para a de x. Eles não geram código de máquina nenhum. São marcadores que indicam onde o código de máquina deve ser corrigido.
Patterson mostrou o código de máquina antes e depois da invalidação, num cenário em que o bloco é compilado, o método x é redefinido e o bloco é compilado de novo. Quando a invalidação ocorre, o JIT literalmente sobrescreve o código de máquina: o marcador aponta para uma instrução de teste, que é substituída por um salto para um código de saída. Esse código de saída restaura o estado interno e a pilha da VM, e a execução continua na máquina virtual do Ruby.
A detecção depende do próprio CRuby. Quando um método é redefinido, os caches precisam ser invalidados de qualquer forma, e esse é o momento ideal para o JIT invalidar também o código compilado. O compilador registra quais métodos compilou e, quando algum é redefinido, invalida o código de máquina associado, tanto no YJIT quanto no ZJIT.
A redefinição de métodos não é a única invariante monitorada. Patterson citou as operações básicas (BOPs), por exemplo garantir que + continue significando soma; as constantes, que não podem ter sido redefinidas; e os TracePoints, porque um TracePoint ativo permite observar coisas que o JIT pode ter otimizado e removido, o que quebraria a regra "as-if". Há outras, e ele convidou quem tivesse interesse a conversar depois.
"IA" no ZJIT: interpretação abstrata e a eliminação de alocações
Patterson brincou que, já que estava no "AI World", o ZJIT também usa muita "IA", mas no sentido de abstract interpretation, interpretação abstrata. Aproveitou para convidar quem quisesse "trabalhar com IA" a contribuir com o ZJIT. Interpretação abstrata, explicou, é fingir executar o código e deixar que as otimizações surjam a partir dessa simulação.
O exemplo foi um servidor de teste que passa cerca de 1.000 requisições falsas por uma aplicação Rack e mede quantos objetos foram alocados. Sem JIT, são cerca de 1.000 objetos, o que faz sentido para 1.000 requisições. Com YJIT, o número é praticamente o mesmo. Com ZJIT, são 8 objetos.
As alocações vêm do array devolvido pelo método call da aplicação Rack, com status, cabeçalhos e corpo. Patterson reconstruiu o raciocínio do compilador passo a passo:
- O ZJIT coloca a chamada a
callinline dentro do métodoserve. - O código é reescrito com variáveis temporárias. Status, cabeçalhos e corpo passam a ser atribuídos a variáveis intermediárias, o que não altera o comportamento do programa.
- O interpretador abstrato percorre o código mantendo um heap abstrato, que registra como os objetos ficariam se o programa fosse executado.
v1recebe 200,v2recebe a constante dos cabeçalhos,v3recebe o corpo ev4recebe um array abstrato contendov1,v2ev3, sem que importe quais são esses valores. - Status, cabeçalhos e corpo são lidos dos elementos do array. Como o heap abstrato sabe que o primeiro elemento é
v1, e assim por diante, dá para fazer substituição algébrica. - Depois da substituição,
v4não é usado em lugar nenhum, então pode ser eliminado. Com ele desaparece a alocação do array. - As variáveis intermediárias também podem ser removidas, com atribuições diretas. O processo se repete com mais inlining.
Combinando inlining, propagação de constantes e interpretação abstrata, o ZJIT transforma o código original numa versão sem aquelas alocações. Patterson recapitulou as camadas vistas até ali (paralelismo com Ractors, eliminação de quadros de pilha no interpretador, inlining e eliminação de alocações no JIT) e perguntou à plateia se essa última otimização viola a regra "as-if", já que a mudança pode ser observada contando alocações. Sua resposta pessoal foi que ele não se importa e acha ótimo reduzir as alocações.
A segunda história: RubyGems e o incidente com a OpenAI
Patterson disse que ia encerrar ali, mas que na verdade eram "duas apresentações em uma". Com uns 12 minutos restantes, contou da sua perspectiva um episódio recente da comunidade: o incidente envolvendo o RubyGems e a OpenAI. Ele comentou que não tinha intenção original de falar do assunto e que passou o dia anterior inteiro montando os slides.
Nos dias 11 e 12 de maio, o RubyGems.org começou a receber milhares de gems indesejadas. Depois isso seria chamado de campanha "gem stuffer", mas na época ainda não tinha nome. Em 12 de maio, Moshe Mansfield, que trabalha no RubyGems.org revisando pacotes novos, tornou o ataque público no Twitter: o RubyGems.org estava sob um grave ataque malicioso e os cadastros estavam suspensos. Em 13 de maio, a Socket.dev publicou um post chamando o caso de "ataque gem stuffer".
Patterson leu o post. A descrição era de gems com um script malicioso que fazia requisições a sites do governo do Reino Unido, baixava os dados, empacotava tudo numa gem e a enviava de volta ao RubyGems.org. O post dizia que era possível saber se alguém tinha sido vítima procurando certos arquivos no sistema de arquivos. Aquilo lhe pareceu estranho. Ele lembrou que, ao instalar uma extensão C, o extconf é executado, o que já é uma forma conhecida de execução remota de código. Mas o script malicioso não estava num extconf. Na avaliação dele naquele momento, instalar a gem não executaria o script, e alguém teria de rodá-lo de propósito. O post não explicava como nem em que circunstâncias o código seria executado. Patterson achou esquisito e seguiu adiante. Em 16 de maio os cadastros reabriram, e ele não pensou mais no assunto.
O bug de cache que ficou seis anos em produção
Em 6 de julho, Luke Marshall, da equipe de segurança da Truffle, notificou a equipe do RubyGems.org sobre um problema de cache. Patterson esclareceu que trabalha no cliente RubyGems, não no servidor, mas que as equipes conversam e trabalham juntas.
Ele explicou o mecanismo. Pelo lado da vítima, a autorização antiga era feita com uma requisição GET: ao rodar o comando de autenticação do gem, o cliente fazia uma autenticação básica e o servidor respondia com a chave de API em um formato específico. Patterson pediu à plateia que guardasse esse formato na memória. Pelo lado do atacante, a Fastly ficava na frente do RubyGems.org e armazenava em cache as respostas GET. Um atacante que fizesse a mesma requisição sem cabeçalho de autorização podia receber de volta a chave de outra pessoa, vinda do cache.
Segundo Patterson, o problema ficou em produção por cerca de seis anos, e ele apontou alguns motivos para ninguém ter notado. Até onde se sabia na época, não havia indícios de abuso. Para acontecer, era preciso enviar Accept-Encoding: gzip, o que o curl não faz por padrão, embora o Net::HTTP faça. Além disso, um mantenedor que recebesse a chave de outra pessoa e tentasse um gem push veria apenas uma mensagem de que não estava autorizado a publicar a própria gem, faria login de novo, o problema sumiria e ele seguiria em frente. Só o código antigo do RubyGems usava esse fluxo, e o cache expirava após uma hora. Três dias depois do relatório, a correção foi lançada e o problema, segundo ele, ficou totalmente resolvido.
O e-mail inesperado e o script "leaker"
Em 8 de setembro, no trabalho, Patterson recebeu uma mensagem direta da colega Emily avisando que chegaria um e-mail de Gemma, ex-colega dele que trabalha na Anthropic, sobre um hack muito grave no RubyGems. Ele respondeu que Gemma poderia simplesmente mandar o e-mail, e Emily lembrou que ele é péssimo com e-mails. Gemma o apresentou a uma pessoa chamada Neev, e Patterson entrou em pânico, procurando freneticamente na caixa de entrada e no HackerOne algum relatório que pudesse ter perdido. Não achou nada e pediu desculpas. A resposta foi que não se tratava de um problema atual do RubyGems: havia uma pesquisadora de segurança tentando falar com alguém do projeto.
Então veio o e-mail de Sydney von Arx dizendo que agentes da OpenAI tinham tentado explorar a vulnerabilidade de cache do RubyGems e que quatro pacotes enviados tentaram usá-la para roubar chaves de API de usuários. A primeira reação de Patterson foi achar aquilo uma besteira. Mesmo assim, "tentando ser uma pessoa responsável", clicou nos links e leu o código.
No trecho que mostrou, ele destacou quatro coisas. Havia um comentário no início sobre "variantes de chaves vazadas". O código estava num arquivo script.rb, dentro de uma gem chamada sln-leaker-5. O script fazia uma requisição GET ao caminho de autorização do RubyGems.org. E no final havia uma expressão regular que batia com o formato da chave de autenticação que ele tinha mostrado antes. Foi aí que entendeu que aquilo estava de fato tentando invadir o RubyGems.org.
Como o rubydoc.info virou o vetor do ataque
Na reunião com a pesquisadora, Patterson voltou à dúvida de maio: o código estava em script.rb, não no extconf, então como alguém o executaria? A resposta foi que ele rodava no rubydoc.info, um servidor que exibe a documentação de gems.
As gems maliciosas incluíam um arquivo .yardopts com uma instrução para carregar ./script.rb. Segundo Patterson, a gem YARD instala um plugin do RubyGems. Quando outra gem é instalada, o plugin procura um .yardopts dentro dela e, se encontra, executa o que ele indica.
O ciclo, como Patterson o descreveu, funcionava assim:
- Um bot envia uma gem ao RubyGems.org.
- O RubyGems.org dispara um webhook para o rubydoc.info, um serviço completamente independente.
- O rubydoc.info baixa a gem e a processa dentro de um contêiner Docker que ainda tinha acesso à rede.
- O script malicioso é executado, baixa dados do governo do Reino Unido e os empacota numa gem.
- Essa gem é enviada ao RubyGems.org, que dispara um novo webhook, e o ciclo recomeça.
Patterson disse que ficou de queixo caído ao ver todas as peças se encaixando e refez a cronologia. As gems da campanha têm data de envio de 11 de maio. O relatório sobre o cache chegou em 6 de julho, enviado por alguém totalmente independente da OpenAI, que descobriu o problema por conta própria. Ou seja, os bots já sabiam da falha um ou dois meses antes. Ele acrescentou que tudo isso aconteceu bem antes da violação de segurança da Hugging Face. Em 12 de setembro, Sydney e sua equipe publicaram a análise em rubyhack.ai, que Patterson recomendou como detalhada e muito boa.
O que o apavorou foi o que os bots descobriram sozinhos: que o RubyGems.org envia um webhook ao rubydoc.info; que o YARD executa código arbitrário; e que o rubydoc.info processa a documentação com acesso à rede. Ele acrescentou uma hipótese, apresentada explicitamente como suspeita. Nem todas as gems da campanha contêm o código que tenta obter chaves. Para ele, isso indica que, quando a equipe do RubyGems.org começou a desativar contas e impedir logins, o bot precisou de outra forma de se autenticar sem credenciais e passou a explorar a vulnerabilidade de cache, então um zero-day. Patterson chamou o conjunto de máquina de Rube Goldberg.
O final, segundo ele, foi feliz. A equipe do RubyGems.org resolveu tudo e fechou os vazamentos, e, até onde se sabe, ninguém foi explorado e ninguém na comunidade sofreu danos. Ele agradeceu à equipe.
Otimista, mas sem se render: a IA não é o seu compilador
Para fechar, Patterson se declarou otimista em relação à IA e depois corrigiu para apenas "otimista": disse que este é o melhor momento para estar vivo e que acredita que as coisas vão melhorar. Usa IA todos os dias para escrever código. Mas, para ele, ser otimista sobre a IA ou sobre o futuro não significa se render a ela.
Ele citou um argumento que já ouviu: a IA seria só o próximo compilador, e, assim como ninguém lê o código de máquina gerado pelo compilador, não haveria motivo para ler o código gerado pela IA. Patterson disse entender o argumento até certo ponto, porque de fato quase ninguém lê o código de máquina do compilador. A diferença, segundo ele, é que o compilador fez uma promessa, a regra "as-if": faça o que fizer, o comportamento observável corresponde ao código que você escreveu. Tudo o que ele mostrou na palestra, dos Ractors aos patch points do ZJIT, ilustra o custo de cumprir essa promessa.
A IA não fez essa promessa, disse ele, e não existe regra "as-if" para o inglês. Por isso Patterson afirmou que não vai entregar seu destino à IA, que vai continuar lendo o próprio código e que espera que a plateia faça o mesmo. Encerrou com o slide "Get in, loser, we're going programming" ("Entra aí, perdedor, vamos programar").
Olá a todos. Olá. Olá. Sim. Antes de começarmos, gostaria de fazer um breve anúncio. Comecei a desenvolver minha própria distribuição do Linux.
Eu queria mostrar a vocês como ele é inicializado, mas não pude colocar o computador no palco, então o deixei com a equipe de audiovisual. Por isso, vou pedir para eles darem início à minha apresentação agora, por favor. Ok. Tudo bem. Ok. Eles vão ligar o computador.
Tudo bem. Quando você puder abrir o PowerPoint para mim, quando… Ah, meu Deus.
A propósito, eu fiz isso com vibe coding.
Tudo bem. Ok. Então, acho que é melhor começarmos. Vou chamá-lo de AaronXP. É o nome desse software. A instalação leva 100 milissegundos. E é muito importante para mim reduzir o tempo de instalação, porque faço isso o tempo todo. Acordo de manhã e instalo meu sistema operacional.
Tomar um café, instalar meu sistema operacional, atualizar o Chrome, reinstalar o sistema operacional. Quer dizer, qual é!
Acho engraçado, acho irônico fazer uma piada sobre a crise financeira e, em seguida, incentivar todo mundo a criar um monte de código em Rust sem nem mesmo lê-lo. Vou começar a vender seguros contra dívida técnica, então venham me procurar depois, se quiserem comprar algum.
Eu sei, eu sei que todo mundo aqui adora usar IA. Eu também. Adoro usar IA. Adoro usá-la para fazer granadas de lama. É ótimo. Muito bom.
Mas eu quero fazer uma enquete com o público aqui. Acho que, sim, vou fazer uma enquete. Ok. Quantos de vocês gostam de receber granadas de lama? Alguém pode levantar a mão? Ok, temos 2, 3. Ah, algumas pessoas dedicadas aqui na frente. Ótimo, obrigado. É bom ver isso.
Eu também recebi alguns… Muito obrigado, Amanda, pelo seu trabalho árduo aqui. Podemos dar uma salva de palmas para ela? Sim.
Então, quando ela estava exibindo os registros durante essa conferência, alguém tirou uma foto minha aqui na plateia. Esse sou eu.
Tive que contar isso pra ela depois. E aí pensei: “Agora você não pode… Essa piada é minha. Agora você não pode contar isso no palco. É minha. Vou contar eu mesmo.”
Tudo bem. Antes de fazer mais piadas hoje, quero fazer algo um pouco mais sério. Vou chamar alguém aqui da plateia, uma pessoa específica, e vou deixá-la na vergonha. E ele está bem ali. Mike Dalessio.
Então, não sei se vocês viram a palestra dele, mas o Mike postou isso. Nós nos conhecemos há mais de 20 anos. Ele me enviou um pull request de código aberto para o Mechanize, uma biblioteca em Ruby da época. E, desde então, venho trabalhando com ele na comunidade de código aberto. Cheguei até a trabalhar com ele na Shopify por um tempo. Na verdade, ele era meu chefe lá.
Começamos juntos, criamos uma organização no GitHub chamada Sparkle Motion. Eu sou a Sparkle, e ele é o Motion nessa parceria. O que isso significa é que eu subo no palco com gestos teatrais, enquanto ele, na verdade, faz o trabalho de verdade.
Trabalhamos juntos, colaboramos em muitos, muitos projetos de código aberto. Por exemplo, o Nokogiri, talvez você conheça. Já vi isso em vários slides sobre instalações lentas de gems. Também trabalhamos com a gem SQLite3, e ele é muito, muito ativo na equipe de segurança do Rails.
Mas o anúncio que quero fazer aqui hoje é que ele também é o mais novo membro da equipe principal do Rails. Então… Obrigado. Obrigado, Mike. Agradeço muito, muito, muito mesmo pelo seu trabalho. Não sei se o site já foi atualizado ou não, mas, de qualquer forma, obrigado. Estou ansioso por isso — muito obrigado mesmo. E nós… fico feliz que você continue ativo, apoiando a gente.
Sim, sim. Tudo bem, é emocionante estar aqui no Rails World. E sei que muita gente já fez essa piada antes, mas eu também estou usando o Claude para gerar meus slides. Então, é ótimo estar aqui no AI World, no Texas.
Foi legal aprender como podemos usar a IA — todos nós —, todos podemos usar a IA para nos tornarmos programadores 1.000x melhores. Então, todos nós aqui presentes hoje podemos usar a IA para nos tornarmos programadores 1.000x. Ah, desculpem, esperem um pouco, cometi um erro de digitação. Quero dizer, 1.000 ex-programadores.
Tem alguém aqui que já andou nos táxis robóticos? Para mim, isso é incrível. Sim, sim. Se você ainda não andou neles, recomendo muito que experimente. É muito, muito divertido. Mas não se esqueça de levar um amigo com você, porque assim é muito mais divertido.
Sim. Tudo bem. Feliz sexta-feira. Uhu! Feliz sexta-feira a todos. Em algum lugar do mundo, é sempre sexta-feira.
Meu nome é Aaron Patterson. Também sou conhecido como Tenderlove. Trabalho na equipe de infraestrutura do Ruby on Rails na Shopify e ajudo em várias tarefas relacionadas à equipe. Lá, eu me dedico a várias coisas. É muito divertido. Eu também tenho um gato. Este é o meu gato. Talvez vocês já o tenham visto aqui em um dos balcões de check-in do Passport.
Hoje vou falar sobre desempenho, obviamente, porque é disso que sempre falo nas apresentações. Não sei mais sobre o que falar. Vou falar sobre Ractors. Vou falar sobre o ZJIT. E, claro, vou falar sobre IA, já que estamos no AI World. Então, é isso que vamos fazer.
E o motivo pelo qual vou falar sobre essas coisas — o importante é que o motivo é que, este ano, tenho trabalhado com desempenho, com Ractors, com o ZJIT e também com IA. E sim, na verdade estou repetindo esses slides porque fico muito, muito nervoso quando estou preparando apresentações, com medo de não ter conteúdo suficiente para a conferência, então acabo adicionando mais slides. Mas sim, é sobre isso que vou falar hoje.
Este ano também escrevi um alocador de registradores para o ZJIT, e não vamos falar sobre isso hoje, mas queria incluir isso aqui porque esse foi o projeto mais difícil em que já trabalhei. E estou muito, muito orgulhoso do meu trabalho aqui. E eu só queria, tipo, colocar isso aqui e dizer que fui eu quem fez isso. E foi ótimo. Então, estou orgulhoso de mim mesmo por ter feito isso. Ah, obrigado.
Também ganhei um novo gerente este ano. E acho que as coisas estão indo muito bem com ele. Não tenho certeza. Gravei minha primeira reunião individual com ele e vou compartilhar a transcrição com vocês hoje; espero que possam me ajudar a avaliar isso.
Então, na nossa primeira reunião individual, ele me disse: “Aaron, você só tem testado caminhos de código que geram erros”. E eu disse: “Ah, bem, isso é porque estou esperando matrizes.”
É porque meu código é excepcional.
E ele não ficou muito satisfeito com isso, pelo que me parece. Mas eu queria tranquilizá-lo, então disse a ele: “Não se preocupe, essas reuniões só vão melhorar. Eu vou garantir isso.”
Sim. Então, acho que as coisas estão indo bem. Não tenho certeza.
Vou começar minha apresentação com uma citação. O objetivo de um sistema é o que ele faz. Já ouvi essa frase muitas vezes antes e, tipo, nunca parei para pensar muito no que ela significava. Não sei. Isso me faz pensar, e eu não gosto de fazer isso, então não dei muita importância ao assunto.
Mas acho que falei sobre isso com meu gerente e pensei comigo mesmo: “Tudo bem, quando estou tentando entender para que serve um sistema, preciso entender o que ele faz”. E como meu gerente era novo aqui, ele me perguntou: “Aaron, tipo, o que é isso? Para que serve esse sistema em que você está trabalhando?” E eu disse a ele: “Bem, é isso mesmo, o que o sistema faz?”. E ele me mandou ir me foder.
Estou brincando. Estou brincando mesmo. Ele não diria isso pra mim. Eu disse que o objetivo dele é gerar exceções. Então, não tenho certeza. Acho que essas conversas individuais estão indo bem. Não sei. Não sei. Vamos ver depois disso.
De qualquer forma, não gosto muito dessa frase, e o motivo é que ela é muito ambígua. Quando penso nisso, fico tipo: “tá, tudo bem”, mas logo em seguida me pergunto: “o que esse sistema faz? O que ele faz?”. Não sei. Então, não sei qual é o objetivo, e por isso fico pensando: “não sei qual é o objetivo”. Não consigo entender isso. E, pra te dar um exemplo, meu cérebro é pequeno demais pra isso. Não consigo lidar com isso.
Pra te dar um exemplo, digamos que eu tenha viajado no tempo com meu fiel Nokia 3310. Sim. Tenho certeza de que há… Alguém aqui tem esse celular? Sim. Sim. Claro que sim. Tenho certeza de que há pessoas aqui que nunca viram isso antes, mas esse é um celular indestrutível. É ótimo.
Digamos que eu viajasse no tempo com esse celular. E se eu o desse a alguém que nunca tivesse visto um celular antes, essa pessoa poderia até usá-lo como martelo.
Então fico pensando: será que o propósito desse celular é servir de martelo? Tipo, se você interpretasse literalmente a frase que eu disse antes, talvez respondesse que sim. Quer dizer, eles claramente estão usando isso como martelo, então esse é o propósito dessa coisa. Mas, pra mim, seria um celular.
Acho que o que o sistema faz depende de quem está realmente olhando para ele. Então, quando olho para aquele celular, vejo um celular e vou usá-lo como celular. Mas se eu o levasse de volta no tempo, a pessoa que o visse ali o veria como um martelo e o usaria como martelo. Portanto, o que realmente importa é quem está observando aquele sistema.
Assim, se eu pudesse refinar a citação original, diria que a finalidade de um sistema é o que ele faz para aqueles que o observam. Alguém famoso disse isso no Rails World 2026.
Sim. Então, tenho pensado muito sobre isso porque, como disse, tenho trabalhado principalmente com otimizações de desempenho ou otimizações de código. E essa atitude específica de que estou falando, essa atitude observacional, é amplamente conhecida entre os implementadores de linguagens. E é conhecida como a regra “as-if”.
Existe uma regra chamada “regra do como se”, e você pode ler sobre ela na Wikipédia, mas ela está, na verdade, codificada na linguagem de programação C++ no que diz respeito à aplicação de otimizações ao código.
Então, vamos ler um trecho sobre a regra “as-if”. A regra “as-if” diz que, em C++, é permitido aplicar qualquer transformação de otimização a um programa durante a compilação, desde que tal otimização não altere o comportamento observável do programa, conforme especificado pela norma.
Em outras palavras, o compilador tem liberdade para fazer quaisquer alterações que desejar no seu código, desde que o código seja executado da maneira como você o escreveu, da maneira que parece que deveria ser executado.
Agora, há algo um pouco estranho nessa frase específica, nessa citação em particular. Ela diz “conforme especificado pela norma”, algo que praticamente não temos em Ruby. Não existe, digamos, um padrão específico como o do C++. Mas não vamos nos preocupar com isso por enquanto.
Então, hoje quero falar sobre otimizações sob a perspectiva do comportamento observável.
Toda otimização tem dois lados: o que é afetado pela otimização e o que observa essa otimização específica, seja quem for que esteja analisando esse código. Agora, quanto ao primeiro, vamos examinar alguns exemplos desses diferentes observadores e otimizações e ver como eles se influenciam mutuamente.
Então, o primeiro que vou mostrar a vocês — e garanto que praticamente todos já fizeram isso antes, eu acho — é a memoização. Esse é o primeiro, o primeiríssimo exemplo. Tenho certeza de que todos nós já escrevemos esse padrão específico antes.
Temos uma função; sabemos que ela é cara, mas só precisamos calculá-la uma vez. Então, calculamos uma vez e, em seguida, armazenamos o resultado em uma variável de instância. Tenho quase certeza de que muitas pessoas aqui já escreveram código semelhante a esse. Eu, pelo menos, já escrevi.
O único problema com isso — esse padrão de código específico — é que incorporamos uma suposição ao código. E a suposição é que esse código seja executado por apenas uma thread por vez. Portanto, na verdade, existe uma condição de corrida nesse código, na qual poderíamos acabar executando esse cálculo dispendioso várias vezes, mas não percebemos isso porque temos o GVL no Ruby.
Bem, é mais ou menos aí que entram os Ractors. Os Ractors nos permitem ter um verdadeiro processamento paralelo em nosso código Ruby. Isso significa que os observadores do nosso código mudaram. Na verdade, podemos ter duas threads acessando exatamente esse caminho de código ao mesmo tempo. Isso significa que podemos ter vários observadores de um determinado caminho de código em paralelo.
Felizmente, os Ractors lançam uma exceção para nós nesse caso. Assim, em vez de ocorrer uma condição de corrida e causar algum problema no seu aplicativo, você receberá uma exceção e terá a oportunidade de corrigir o problema.
Espero que vocês tenham assistido à apresentação do Andrew. Ele falou detalhadamente sobre esse tipo de coisa, mas vou repetir um pouco disso aqui. Mas, quando digo isso, não quero que você se assuste e pense: “Bem, eu nunca deveria usar isso, nunca deveria usar esse padrão específico”.
Vou mostrar alguns exemplos desse padrão. No primeiro caso, temos vários Ractors acessando essa variável de instância memoizada na classe Foo. E o que isso significa é que temos uma condição de corrida na classe Foo, e o bom é que esse Ractor R1 vai de fato nos dar uma exceção dizendo: “Ei, você não pode fazer isso”. Você precisa fazer isso de maneira segura para threads.
Agora, o segundo caso… na verdade, o segundo caso está correto. Você pode ver que o padrão é o mesmo. Podemos usá-lo. Está tudo muito, muito bem assim. A questão é que, no segundo caso, temos apenas um observador desse caminho de código específico. Portanto, não há problema em usar esse padrão. Só precisamos garantir que ele esteja isolado a um Ractor específico.
Agora, felizmente, assim como esse padrão de variáveis de instância no nível da classe, vemos isso ser bastante comum em várias bases de código. E, na verdade, haverá uma maneira de mitigarmos isso ou lidarmos com essa situação na próxima versão do Rails. E isso será feito com o módulo ActiveSupport/Ractors.
Com o módulo ActiveSupport/Ractors, existe um método `on_main` que nos permite executar determinado código no Ractor principal. Portanto, se tivermos um cálculo demorado, precisamos que ele ocorra dentro do Ractor principal e armazenar algo em uma variável de instância no nível da classe. Podemos fazer isso.
Agora, se você assistiu à apresentação do Andrew, sabe que isso causa latência no seu aplicativo. E a razão pela qual isso causa latência é que, se você transferir uma grande quantidade de cálculos para o Ractor principal, de repente esse Ractor se torna o gargalo do seu aplicativo. E você acabará observando um aumento na latência, pois todos estão disputando esse Ractor.
Portanto, sempre que possível, recomendo fortemente o uso da gem “Ractor sharing”. Ela foi escrita por Koichi Sasada, que é o autor principal do Ractor. Então, espero que isso funcione muito bem como código baseado no Ractor. Na verdade — não “espero”, mas sim “vai funcionar”. Então, vocês deveriam usar isso.
Ela contém muitas estruturas de dados diferentes compatíveis com o Ractor. Vou compartilhar algumas delas aqui. Há duas aqui que gostaria de destacar, pois resolvem esse problema específico que estávamos analisando.
A primeira são as variáveis de bloqueio (lock variables) e a segunda são as TVars; a escolha depende da taxa de processamento que você deseja para sua aplicação. Uma variável de bloqueio garante que um cálculo específico ocorra apenas uma vez, enquanto a TVar permite que ele ocorra uma ou mais vezes — basicamente, algumas vezes.
Portanto, a primeira é usada quando você tem um cálculo que não é idempotente e precisa garantir que ele ocorra apenas uma vez; já a segunda é indicada quando o cálculo é idempotente e, digamos, talvez seja aceitável que isso aconteça algumas vezes.
Além disso, há muitas outras estruturas de dados ali. E não quero assustar vocês a ponto de deixarem de usar essas estruturas de dados, porque acho que a natureza da implementação dos Ractors significa que, mesmo se usarmos essas estruturas de dados, não haverá nenhum impasse em nossas aplicações, nem problemas de segurança de threads, pois, em vez disso, receberemos apenas exceções.
E a razão pela qual acho que não devemos ter muito medo dessas estruturas de dados
é que acredito que o alcance dos problemas seja bastante limitado. Então, vamos dar um exemplo. Aqui temos um servidor web simulado baseado em Ractor. Ele simplesmente lê uma solicitação do soquete em um loop infinito e, em seguida, atende à solicitação dentro de um Ractor.
E se pensarmos no ciclo de vida normal de solicitação-resposta desse processo, estamos alocando a maioria dos nossos objetos dentro desse método `serve`. Portanto, todas as nossas alocações ocorrerão lá dentro. E, na verdade, a única vez que realmente precisamos nos preocupar com isso é quando acessamos elementos externos. Agora, é claro, a gente mexe em coisas externas, como constantes ou valores de configuração, coisas desse tipo, mas acho que o número de pontos de intervenção é bem menor do que a gente imagina.
Outro assunto sobre o qual quero falar um pouco é a observabilidade de dados e como ela afeta o desempenho. O Ruby 4.1 será lançado com um coletor de lixo local do Ractor. Então, o que isso significa?
No GC, o coletor de lixo do Ruby é um singleton. Então, sempre que um Ractor precisa alocar um objeto, ele vai pedir ao coletor de lixo: “Ei, me dê um objeto”. E se tivermos vários Ractors rodando em paralelo, fica bem claro que vamos enfrentar um gargalo aqui. Vamos tentar alocar um monte de coisas, e aí todos esses Ractors ficam esperando que o GC aloque tudo.
Por isso, no Ruby 4.1, teremos um coletor de lixo local para cada Ractor, o que significa heaps separados para cada um, o que resolverá isso, eliminando esse gargalo específico. Assim, cada Ractor pode solicitar um objeto ao seu heap.
Agora, talvez também possamos aproveitar isso para obter uma coleta de lixo mais eficiente. Por exemplo, neste nosso servidor web simulado, podemos criar um novo Ractor a cada solicitação. E, quando a solicitação for concluída, podemos simplesmente liberar o heap e continuar o processo. Portanto, teoricamente, podemos obter coletas de lixo mais eficientes com isso.
Bem, eu gostaria que a história acabasse por aí, mas ela é um pouco mais complicada do que isso, e quero mostrar a vocês um exemplo do porquê. Digamos que tenhamos esse exemplo de produtor-consumidor em Ruby. Peço desculpas. Peço desculpas por fazer vocês lerem código.
O produtor aloca um objeto em seu próprio heap dessa forma e, em seguida, passa esse objeto para um consumidor. Assim, quando isso acontece, tanto o consumidor quanto o produtor estão apontando exatamente para o mesmo objeto, e você pode ver isso pelo ID do objeto aqui. Os dois Ractors são capazes de observar exatamente o mesmo objeto ao mesmo tempo.
Portanto, o que isso significa é que, quando um desses Ractors é encerrado, isso não significa necessariamente que possamos descartar o heap, pois pode haver outros Ractors apontando para esse heap. Portanto, precisamos de alguma maneira de resolver essa situação. A maneira como resolvemos isso é por meio de um coletor de lixo global, um GC global, e você pode pensar nisso como uma coleta “major”, só que, idealmente, ocorre com menos frequência do que uma coleta “major”.
Então, quero mostrar mais um exemplo. No exemplo anterior, passamos propositalmente um objeto de um Ractor para outro, e ambos conseguiram ver esse objeto. Quero mostrar um exemplo diferente, no qual fazemos isso implicitamente. Mostrei esse exemplo no Rails World no ano passado, mas acho que este é um ótimo exemplo.
Temos duas análises de JSON. Estamos analisando dois documentos JSON e, em seguida, examinando a chave no documento JSON; e você verá aqui que essas chaves correspondem exatamente ao mesmo objeto. Elas têm o mesmo ID de objeto. Podemos observar que se trata do mesmo objeto.
Agora, se pegarmos esse código e o dividirmos em vários Ractors, veremos que a string “hello” tem o mesmo ID de objeto em ambos os Ractors. Portanto, essas chaves são desduplicadas entre os Ractors, e ambos os Ractors veem a mesma string “hello”.
E o que está acontecendo nos bastidores é que estamos alocando uma string “hello” em um heap — nosso heap principal do Ractor — e, então, os outros dois Ractors estão apontando para esse heap. O que é interessante nesse exemplo é que isso possivelmente significa que estamos afetando o desempenho do GC, mas não sabemos disso. Isso está acontecendo implicitamente.
Então, quero dar um passo atrás por um minuto. Temos analisado assuntos um pouco mais a fundo aqui. Começamos analisando as otimizações que os desenvolvedores fazem. Assim, vimos como os Ractors adicionam um novo observador ao sistema e os impactos a jusante dessa adição. Agora, quero analisar as otimizações do framework e da linguagem e, especificamente, quero examinar as inconsistências no backtrace.
Então, aqui temos um exemplo de um callback de salvamento. Estamos usando os callbacks do ActiveSupport. Estamos criando uma espécie de ActiveRecord simulado aqui, onde você pode incluir isso e, em seguida, chamar o método `save`, obtendo assim um callback de salvamento. E esse código de exemplo exibe a pilha de chamadas de dentro do nosso callback de salvamento.
Se imaginarmos o rastreamento da pilha a partir disso, podemos imaginar que veremos, primeiro, o bloco para o `run_callbacks` e, em seguida, o quadro do método `save`. E como estamos executando isso apenas a partir de um script, veremos `main` como o nível superior.
Então, vamos pegar esse módulo e incorporá-lo a uma classe, uma classe sem callbacks. Esse é, de certa forma, nosso caso base aqui. Se executarmos isso, veremos que, de fato, ele tem apenas 3 frames de pilha.
Então, vamos tentar de novo, mas desta vez com 1 callback. Nesse caso, vamos definir um before_callback e analisar o backtrace. E vemos que, mais uma vez, há apenas 3 frames aqui. Então, isso é ótimo, e é o que esperávamos.
Agora, vamos fazer mais um. Vamos definir um callback “around”. Se fizermos isso, veremos que nosso rastreamento de pilha, de repente, disparou. Agora, lembre-se: não alteramos nada do código que estava dentro desse método `save`, e, de repente, nosso rastreamento de pilha está completamente diferente.
Bem, enquanto eu investigava isso, achei muito engraçado encontrar este comentário no ActiveSupport. Vou ler. Diz o seguinte: “Como esse método é usado em muitos lugares e frequentemente envolve grandes partes do código do usuário, ele tem um objetivo de design adicional de minimizar seu impacto na pilha de chamadas visível. Uma exceção gerada dentro de um callback ‘before’ ou ‘after’ pode causar todo o ruído que quiser, mas quando o controle é passado suavemente para o bloco fornecido, queremos o mínimo possível de indícios de que estivemos aqui.” E essa é exatamente a regra do “como se” de que falávamos anteriormente.
Então, nesse caso, estávamos vendo mais pilha do que o esperado, e isso estava acontecendo dentro do Rails. E eu posso perdoar isso porque temos, digamos, um código de aplicação que está realizando mais tarefas. Nós adicionamos um callback “around”, então, de certa forma, sabemos que, sim, tudo bem, ele está trabalhando mais, então é claro que haverá mais rastreamento de pilha.
Mas será que é possível obter menos quadros de pilha do que o esperado? Já vimos esse caso? Sim, já vimos. Mas, antes de mais nada, sim, eu gosto mesmo de passar meus fins de semana contando quadros de pilha. Mas não acho que isso me torne um perdedor.
Então, aqui temos um exemplo de um quadro de pilha do `new` faltando. Vou mostrar o que é isso. Nesse código, temos o método 1, que chama o método 2 e, em seguida, chama o 3, que aloca `Foo`. O `Foo` chama o 4 de dentro do método `initialize`. E o motivo pelo qual estou fazendo isso é apenas para deixar o rastreamento da pilha um pouco mais claro.
Então, se executarmos esse código, esperamos ver um rastreamento de pilha como este. Veremos: initialize, new, 3, 2, 1, main. Nada de surpreendente. Se executarmos o código no Ruby 3.4, é isso que vemos. Esse é exatamente o rastreamento de pilha que vemos no Ruby 3.4.
Se executarmos isso no Ruby 4.0, veremos que fica um pouco diferente. E, de fato, no Ruby 4.0, esse quadro `Class#new` não existe mais. Ele sumiu. Então, o que aconteceu com esse quadro da pilha? Acho que podemos encontrar algumas pistas.
Vamos descobrir isso aqui. Se analisarmos as instruções do método 3, acho que vamos conseguir aprender alguma coisa. Então, vamos analisar as instruções da VM desse método específico. Na parte de cima, temos as sequências de instruções do Ruby 3.4. E na parte de baixo, temos as sequências de instruções do Ruby 4.0.
E a primeira coisa que podemos perceber a partir disso é que o Ruby 4.0 gera muito mais instruções do que o Ruby 3.4. Não vou explicar essas instruções em detalhes, mas a mudança básica é que incorporamos a chamada a `initialize` no local de chamada do `new`. Exatamente aí. Fizemos uma implementação inline.
Então, se pegarmos essas instruções e as reescrevermos como código Ruby, à esquerda está o código que escrevemos, e à direita está, digamos, o pseudocódigo das instruções que geramos. O pseudocódigo das instruções basicamente diz: “ei, se o método `new` em Foo for a implementação padrão, então o que vamos fazer é alocar um novo Foo, chamar diretamente o método initialize e, em seguida, retornar o objeto. Se não for o `new` padrão, então vamos seguir um caminho mais lento e simplesmente chamar o método `new`”.
Portanto, faz sentido que haja mais instruções aqui, pois ele está simplesmente fazendo mais do que o Ruby 3.4 fazia. Outro ponto a ser observado aqui é que, na verdade, estamos chamando o método `initialize` a partir do método `3`. Antes, o modelo era o seguinte: chamávamos `new`, e `new` chamava `initialize`. Agora, podemos ver aqui que `3` está chamando `initialize` diretamente. Portanto, faz sentido que esse quadro de pilha do `new` esteja faltando.
Então, digamos que tenhamos esse código. É a nossa classe Foo. Imprimimos o rastreamento da pilha do método `initialize` antes de implementar o `new`. Então, basicamente, vamos aplicar um “monkey patch” no `new`. Mas não é um “monkey patch” nada complicado. Estamos apenas chamando o `super` de dentro dele. Então, aqui vamos exibir o rastreamento da pilha antes do `3`. Vamos adicionar `new` e, em seguida, chamar o método novamente e exibir o rastreamento da pilha mais uma vez.
Se fizermos isso, à esquerda está nosso rastreamento de pilha do caminho rápido e, à direita, o do caminho lento, e podemos ver que nossa chamada ao método `new` da classe está de volta. Então, por que nos demos todo esse trabalho? Por que fazer tudo isso? A razão é que, com isso, conseguimos alocações mais rápidas. Neste exemplo específico, a alocação é 70% mais rápida no Ruby 4.0 do que no Ruby 3.0. E isso sem um compilador JIT. Portanto, obtemos ganhos de desempenho realmente bons com essa otimização específica.
Então, o que a gente removeu nesse caso foi um quadro de chamada, e os observadores disso seriam, por exemplo, o `caller`, depuradores e outros elementos que poderiam inspecionar a pilha. Esses são, portanto, os elementos que foram afetados. O que a gente ganhou com isso, porém, foi um aumento de 70% na velocidade das alocações, o que eu acho que é uma boa troca.
E o CRuby faz isso o tempo todo. Há muitos pontos diferentes em que ele faz isso. Vou dar outro exemplo aqui bem rapidinho. Aqui temos uma classe Foo de exemplo que implementa o método `hash`. Ela exibe o rastreamento da pilha sempre que algo chama o método `hash` nela. Acionamos um cálculo de hash e exibimos o rastreamento da pilha. Portanto, esperaríamos que essas duas primeiras chamadas tivessem o mesmo rastreamento da pilha, e que a última tivesse um rastreamento diferente, mas com a mesma profundidade.
E, se observarmos, as pilhas são completamente diferentes. Aqui, na verdade, conseguimos omitir o método `square, square` no lado mais à esquerda. Então, você verá que esse quadro está faltando, enquanto os outros dois lados mostram tudo. O CRuby faz isso o tempo todo. Ele lida de forma bastante flexível com o rastreamento da pilha.
Mas acho que a questão é: quem se importa? Sério, alguém se importa com isso? Não. Alguém? Ah, algumas pessoas. Tudo bem. Eu sei que acho isso interessante. Mas, na verdade, é exatamente isso que quero que você sinta.
Se vocês realmente se importassem se essas coisas estavam ou não no quadro da pilha, não seríamos capazes de implementar esse tipo de recurso. É por isso que fico muito, muito feliz que a maioria de vocês pense: “Bem, não me importo com isso”.
Acho que esse tipo de coisa é como se fossem áreas cinzentas nas regras. Então, o compilador fez uma alteração no código, de modo que a execução, estritamente falando, não é a mesma. Dá para perceber uma diferença, mas esses são casos em que as pessoas simplesmente não se importam. Elas realmente não se importam, então tudo bem.
E no Ruby — e acho que na maioria das outras linguagens — precisamos manter o comportamento onde ele realmente importa. Então, esses casos, na verdade, não importam. Estamos obtendo otimizações de desempenho, mas abrindo mão de coisas que não importam de verdade.
Então, acho que esses tipos de otimizações são o que gosto de chamar de otimizações de baixo risco, nas quais alteramos o comportamento da linguagem, mas as pessoas, em geral, realmente não se importam. Elas apresentam baixo risco.
Então, a seguir, quero examinar algumas otimizações de alto risco, especificamente dentro do ZJIT. O ZJIT é capaz de realizar otimizações de alto risco porque consegue reverter a otimização caso algo inesperado aconteça. Então, vamos dar uma olhada em algumas delas.
O ZJIT possui, internamente, dois tipos de representação intermediária. Vamos analisar esses tipos. A representação intermediária, também conhecida como IR, possui um IR de alto nível, ou HIR, e um IR de baixo nível, ou LIR. Se você quiser testar isso em casa, pode visualizar o HIR adicionando essas opções ao seu binário do Ruby. Portanto, se você adicionar “ZJIT dump HIR”, poderá ver a saída e dar uma olhada nela.
Uma das suposições que fazemos no ZJIT é que você provavelmente não redefinirá métodos. Provavelmente não fará isso. Mas, se você fizer, precisamos saber, porque, nesse caso, precisamos agir corretamente. Então, digamos que tenhamos um exemplo como este, em que alocamos um ponto e, em seguida, chamamos um método, que, por sua vez, chama um método nesse ponto.
Se gerarmos o HIR para isso, ele ficará assim, e vou destacar algumas coisas dentro desse HIR. Primeiro, quero mostrar que temos um bloco aqui. Então, podemos ver que este é o HIR de compilação do bloco, em `5.times`. A próxima coisa que quero destacar é que colocamos o `get` como inline no bloco, mas inserimos um frame inline. Portanto, estamos inserindo um frame inline para o `get`, de modo que ele fica como inline dentro do bloco.
A próxima coisa que quero mostrar é que conseguimos, de fato, mover essa constante 42 para dentro do bloco. Assim, o ZJIT conseguiu pegar o código à esquerda e compilá-lo para o código à direita. Ele conseguiu simplificar tudo isso.
Mas como o ZJIT consegue fazer apostas tão ousadas? Nós, talvez, não nos importemos se um quadro de pilha estiver faltando, mas se o seu código redefinir `get` ou `x`, você provavelmente se importaria, porque agora o seu código não está se comportando da maneira que você esperava. Ele não está seguindo a regra “como se”.
Então, se voltarmos à saída do HIR, veremos algumas linhas de pontos de patch. Vou destacá-las. Temos uma aqui. Há um ponto de patch para a redefinição do método `get`. E temos outro ponto de patch definido aqui para a redefinição do método `x`.
E esses pontos de patch não representam nenhum código. Portanto, se você observar a saída do código de máquina, não há nada no código de máquina que represente esses pontos de patch. São simplesmente marcadores. Nós os usamos para saber em que parte do código de máquina devemos aplicar o patch.
Então, digamos que tenhamos um código como o do nosso exemplo anterior. Nesse caso, vamos compilar em JIT o bloco do `5.times`.
Em seguida, vamos redefinir o método x e compilá-lo novamente em JIT. Se fizermos isso, eis um trecho do código de máquina que o JIT gera, e vamos mostrar como fica antes e depois da invalidação. Então, esta é a versão antes da invalidação.
Portanto, quando o método é invalidado, o compilador JIT literalmente sobrescreve o código de máquina. Assim, ele pega esse código de teste. Temos um marcador de invalidação que aponta para a instrução de teste e, quando esse método for redefinido, simplesmente sobrescreveremos essa instrução de teste com uma instrução de salto, e essa instrução de salto irá para um código de saída. Esse código de saída restaurará todos os componentes internos da VM e a pilha da VM, e você continuará executando seu código na máquina virtual do Ruby.
Então, como detectamos esse caso? Tipo, como sabemos que você redefiniu um método? Fazemos isso com a ajuda do CRuby, a implementação do CRuby. Sempre que um método é redefinido, precisamos invalidar os caches de qualquer maneira, e esse também é o momento perfeito para o compilador JIT invalidar qualquer código JIT. Assim, o compilador JIT pode simplesmente rastrear quais métodos ele compilou e, se alguém redefinir um método, ele poderá invalidar qualquer código de máquina associado a esse método. Então, aqui vamos dizer: “Ah, um método foi redefinido”. Vamos invalidar todo o nosso código JIT para o YJIT ou o ZJIT, dependendo de qual você estiver usando.
Portanto, a redefinição de métodos não é a única invariante que monitoramos. Também monitoramos itens como as chamadas de operações básicas, ou BOPs. Por exemplo, no caso do método “plus”, precisamos garantir que “plus” continue significando “soma”. Monitoramos constantes, portanto, precisamos garantir que você não as redefina, ou que haja pontos de rastreamento; pois, se você executar um ponto de rastreamento, isso significa que poderá observar elementos que o compilador JIT possa ter otimizado e removido, e precisamos corrigir isso. Isso não segue a regra do “como se”. E há várias outras também; se você quiser saber mais sobre as outras, por favor, venha conversar comigo depois.
E, como eu disse, eu falaria sobre IA porque estamos no AI World. E o ZJIT utiliza bastante IA. Nós a usamos bastante. E, infelizmente, sei que isso vai decepcionar algumas pessoas, mas estou me referindo à interpretação abstrata.
Uhu! Então, se você quiser trabalhar com IA, venha trabalhar no ZJIT, por favor. Adoraríamos contar com a sua ajuda.
O que é interpretação abstrata? Interpretação abstrata é quando fingimos executar um código. Vamos simular a execução do código e, a partir daí, permitir que as otimizações surjam naturalmente. Então, vou mostrar um exemplo para ajudar a explicar o que isso significa exatamente.
Aqui à esquerda, temos um servidor de teste. Trata-se apenas de um aplicativo Rack de teste. À direita está o nosso servidor real — ou, desculpem, um servidor de teste. À direita está nossa aplicação Rack. E aqui embaixo, na parte inferior, temos apenas um método simples que mede o lixo — mede o número de objetos que alocamos. E ele tenta processar cerca de 1.000 solicitações falsas por meio dessa aplicação Rack.
Se executarmos isso com um compilador JIT, ou se executarmos sem um compilador JIT, serão alocados cerca de 1.000 objetos. Isso faz sentido, pois fizemos cerca de 1.000 solicitações. Se executarmos com o YJIT, veremos que ele aloca aproximadamente o mesmo número de objetos. E, se executarmos com o ZJIT, veremos que ele aloca 8 objetos. Então, de onde vêm esses objetos e como o ZJIT consegue removê-los?
Vamos voltar ao nosso programa de demonstração; fica bem claro a partir dele de onde vêm essas alocações. Estamos alocando uma matriz aqui no método `call`, com nossos cabeçalhos de status e o corpo da mensagem.
Então, já vimos no exemplo anterior que o ZJIT pode fazer inline de funções. O que está acontecendo aqui é que o ZJIT está pegando essa chamada ao método `call` e fazendo inline dela dentro do método `serve`. Então, digamos que a gente tenha feito isso. Vamos incorporá-la aqui assim.
Agora, o que quero fazer aqui é, digamos, pegar esse código e dividi-lo em partes mais simples. Vamos dividir isso em várias etapas, usando variáveis temporárias, e fazer uma parte de cada vez. Então, vamos pegar a atribuição dos cabeçalhos de status e do corpo da resposta e dividi-la. Se fizéssemos isso, acabaríamos com algo assim, em que atribuímos todas essas variáveis a variáveis intermediárias e, em seguida, as usamos dessa maneira. E acho que todos concordamos que o comportamento desse programa é o mesmo do anterior. A única diferença é que temos mais variáveis temporárias.
E é aí que entra a interpretação abstrata. Nosso interpretador abstrato vai percorrer esse código e simular sua execução. Então, vamos percorrer isso agora. A primeira coisa que faremos é criar um heap abstrato. E esse heap abstrato mantém um registro de como os objetos ficariam se executássemos o programa. E vamos simular essa execução. Então, criamos um heap abstrato e, em seguida, armazenamos os valores nele.
Assim, vamos simular a execução. Sabemos que, se executarmos a primeira linha, v1 receberá o valor 200. v2 receberá o valor da constante “headers”. v3 receberá o valor do corpo. E então algo especial acontece com v4. v4 será atribuído a um array abstrato. Portanto, temos um array abstrato aqui. Esse array abstrato contém três valores: v1, v2 e v3. E não nos importamos com quais são os valores de v1, v2 e v3. Não nos importamos nem um pouco com o que eles são. Só sabemos que há esses valores dentro dele.
Depois disso, vamos atribuir “status” ao primeiro elemento da matriz. Em seguida, vamos atribuir “headers” ao segundo elemento da matriz e, depois, “body” ao terceiro elemento da matriz. Agora, a próxima coisa que podemos fazer é dizer: “Bem, vamos realizar uma substituição aqui”. Sabemos que o status é atribuído ao valor v1. Portanto, podemos simplesmente fazer uma substituição algébrica neste caso. E vamos dizer: tudo bem, vamos apenas substituir esses valores.
E, se fizermos isso, há algo que percebemos nesse programa. E é que v4 não é usado. Não usamos a v4 de forma alguma. Como não a usamos, isso significa que podemos eliminá-la. Então, a eliminamos. Depois, vamos ainda mais longe e dizemos: bem, sabe, também não precisamos dessas variáveis intermediárias; então, vamos simplesmente movê-las para os cabeçalhos de status no corpo do código e fazer atribuições diretas. E podemos ser ainda mais ousados. Podemos dizer: “Bem, vamos descer até aqui também”, então eliminaremos todas essas. E podemos incorporar outras funções e executar o mesmo tipo de algoritmo repetidamente.
E, dessa forma, usando a incorporação, a redução de constantes e a interpretação abstrata, conseguimos converter o código da esquerda no código da direita, e é assim que conseguimos eliminar essas alocações.
Então, falamos sobre otimizações e os impactos que os observadores têm nessas otimizações. Falamos sobre paralelismo com os Ractors, eliminações de quadros de pilha no interpretador, além de inlining e eliminação de alocações no compilador JIT. Assim, conseguimos eliminar todas essas alocações neste compilador JIT.
E tenho uma pergunta para todos vocês aqui na plateia. Essa otimização viola a regra “as-if”? Conseguimos observar essa mudança contando o número de alocações no código. Então, será que violamos a regra “as-if”? Você se importa? Eu, particularmente, não. Acho ótimo que estejamos reduzindo isso. Tudo bem.
Eu ia encerrar a apresentação por aqui. Então, ia dizer: obrigado. Conseguimos. Retiramos algumas alocações. Mas, na verdade, tenho outra história para contar. Peço desculpas. Na verdade, são duas apresentações em uma. Vamos passar para o segundo ponto aqui. Ainda tenho 12 minutos. Vamos tentar fazer isso.
Quero falar sobre algo que aconteceu muito, muito recentemente conosco na comunidade e sobre a história que surgiu a partir disso. Acho que é muito interessante. Espero que seja interessante para todos vocês. Quero falar sobre o RubyGems e o incidente com a OpenAI. Alguém já ouviu falar sobre isso? Algumas pessoas, sim. Tudo bem.
Então, espero que essa história seja interessante. E para aqueles que ainda não ouviram essa história, espero que ela continue sendo interessante. Em maio… Quero recontar essa história basicamente da minha perspectiva. Nos dias 11 e 12 de maio, aconteceu algo. O RubyGems.org começou a receber milhares de gems indesejadas sendo enviadas.
Na verdade, quero voltar um pouco atrás. Deixem-me voltar um pouco aqui. Eu ia encerrar por aqui. Obrigado a todos. Vamos dar uma salva de palmas para mim. Uhuuu! Todos esses objetos foram eliminados. Ótimo.
Tudo bem. Vamos animar um pouco as coisas. Então, nos dias 11 e 12 de maio, o RubyGems.org começou a receber milhares de e-mails indesejados — ou, melhor dizendo, “gems” indesejadas — enviadas. Uma quantidade enorme delas. Mais tarde, isso viria a ser chamado de campanha “gem stuffer”, mas na época ainda não havia um nome específico para isso. Quer dizer, quando você tá mandando um monte de coisas pro ar, não dá pra perder tempo pensando num nome.
Em 12 de maio, Moshe Mansfield divulgou o ataque no Twitter. Ele trabalha com o RubyGems.org e verifica os novos pacotes que são enviados. Estavam recebendo uma quantidade enorme de pacotes enviados. Eles disseram: “Tudo bem, precisamos conter o problema. Vamos desativar isso.” Estamos enfrentando um grave ataque malicioso ao RubyGems.org neste momento. Os cadastros estão suspensos. Ele afirmou que estamos enfrentando um grave ataque malicioso ao RubyGems.org e anunciou que os cadastros foram suspensos por enquanto.
No dia 13 de maio, o Socket.dev publicou um post no blog chamando isso de “ataque gem stuffer”. Na época, pensei: “Ok, isso é interessante”. Li o post deles sobre o assunto. O post está aqui, e você pode escanear o código QR aqui. Eles chamaram isso de “gem stuffer”, “ataque gem stuffer” e “campanha gem stuffer”.
Então, eu li o post do blog, e o post dizia mais ou menos assim: “Ok, tem essas gems que estão sendo enviadas, e elas contêm um script malicioso; esse script vai baixar algumas coisas do governo do Reino Unido”. E aí… Na verdade, talvez eu tenha algumas animações aqui. Eu não tinha a menor intenção de falar sobre isso, e passei literalmente o dia todo ontem trabalhando nos slides. Então, isso é… ah, sim, ótimo.
Então, elas têm essa gem maliciosa ou esse script malicioso dentro. O script vai fazer solicitações ao governo do Reino Unido por algum motivo e, em seguida, vai baixar todo o material do governo do Reino Unido. Então ele faria o empacotamento — curiosamente, ele pegaria os dados que baixou, os empacotaria em uma gem e, em seguida, enviaria essa gem novamente para o RubyGems.org.
Na época, achei isso bem interessante, mas, se você ler o código — tipo, aquele que eles mostraram na postagem do blog —, a primeira coisa que me veio à cabeça foi: “Então, eles disseram: ‘Ok, você pode identificar que foi vítima disso porque haverá esses arquivos no seu sistema de arquivos’. Então, você pode encontrar esses arquivos no seu sistema de arquivos.” Eu dei uma olhada nisso e, quando você instala uma gem normalmente — acho que todos sabemos que, ao instalar uma extensão C, por exemplo, o extconf é executado —, já temos ali um problema de execução remota de código. Mas todo mundo sabe disso.
E quando dei uma olhada no exemplo deles, o script malicioso não estava dentro de um extconf. Eles não estavam usando uma extensão C para fazer isso. Então pensei comigo mesmo: “Tudo bem, tanto faz. Tipo, como isso está afetando alguém? Mesmo que você instale essa gem, ela não vai se executar sozinha.” Tipo, você teria que acessar a gem e fazer isso especificamente. Então, isso parece estranho. O artigo nunca aborda como o programa é executado e em que circunstâncias ele seria executado. Então, basicamente, eu só disse, tipo, tudo bem, tanto faz, estranho.
No dia 16 de maio, as inscrições reabriram e, para ser sincero, eu simplesmente não pensei mais muito nisso. No dia 6 de julho, Luke Marshall, da equipe de segurança da Truffle, notificou a equipe do RubyGems.org sobre um problema de segurança relacionado ao cache no RubyGems.org. E eu trabalho um pouco no cliente do RubyGems. Então, eu faço parte da equipe do cliente RubyGems, mas não da equipe do servidor. Mas é claro que a gente conversa, então a gente sabe, sabe como é, a gente trabalha junto.
E o que é interessante sobre esse ataque é que… Vou mostrar a vocês como ele funciona. O que poderia acontecer é que a autorização antiga fosse feita por meio de uma solicitação GET. Então, isso é da perspectiva da vítima. Você faria uma solicitação GET para autorizar gems. Portanto, se você executasse o comando `gem auth`, ele executaria isso. E o que aconteceria é que ele faria uma autorização básica, e então a resposta do servidor seria assim, e você teria esse cache, essa chave lá embaixo. Essa era a sua chave para fazer login. E quero que você tenha esse formato de chave em mente aqui por um segundo. Talvez um minuto, provavelmente mais do que um segundo. Então era assim que ficava.
E, da perspectiva do invasor, a situação se apresentava assim. O que estava acontecendo era que a Fastly estava posicionada na frente do RubyGems.org e armazenando em cache as respostas GET do RubyGems.org. Assim, os invasores enviavam uma solicitação e, de repente, recebiam uma chave armazenada em cache como resposta. Portanto, os invasores não enviam o cabeçalho de autorização, mas conseguiam obter um token de autorização válido em troca. Isso não é nada bom. Não está nada bom. Ficou em produção por uns 6 anos.
É, não é nada empolgante, mas acho que pouca gente percebeu isso porque, se você pensar bem, se analisar o comportamento disso, em primeiro lugar, não havia indícios de abuso na época. Ou seja, não havia indícios de que isso tivesse sido abusado, pelo menos até onde se sabia na época. É preciso definir `accept_encoding gzip` para que isso aconteça. E isso é meio estranho, porque significava que quem usasse o `curl` não perceberia, já que essa opção não vem ativada por padrão. Então, isso simplesmente passou despercebido por muito tempo. No entanto, o Net::HTTP ativa isso por padrão.
E acho que isso também passou despercebido porque, tipo, se você se imaginar como um mantenedor de gem, vai fazer, tipo, um `gem push`. Então, você faria um “gem push” e, digamos, acabaria pegando a chave de outra pessoa. Tipo, você pegou a minha chave, mas vai enviar a sua própria gem. Então, ao fazer o “gem push”, você receberia uma mensagem estranha dizendo que não está autorizado a fazer o envio. Porque você tá tentando usar sua própria gem, mas tá tentando usá-la com a minha chave. Então, simplesmente não funciona, e aí você fica tipo: “Ah, que estranho. Login no gem”. E, de repente, o problema se resolve e funciona, e você simplesmente não liga, né? Tipo, isso poderia ter acontecido.
Então, acho que foi por isso que o problema ficou sem ser percebido por tanto tempo. Além disso, só o Ruby — mais precisamente, o código antigo do RubyGems — usava isso, e o cache expirava após 1 hora. Então, três dias depois, eles lançaram uma correção para isso. O problema foi totalmente resolvido. Portanto, o RubyGems lançou uma correção.
No dia 8 de setembro, eu estava no trabalho. Essa é a minha roupa de trabalho. Recebi uma mensagem direta de uma das minhas colegas, a Emily, e ela me disse: “Aaron, você vai receber um e-mail da Gemma”. A Gemma é uma ex-colega de trabalho minha que está trabalhando na Anthropic. E a Emily disse: “Você vai receber um e-mail da Gemma e vai ser sobre um hack muito grave no RubyGems”. E eu pensei: “Sabe, ela poderia simplesmente me mandar um e-mail, não precisava me mandar uma mensagem direta”. E aí a Emily disse: “Não, você é péssimo com e-mails”. E eu fiquei tipo: “É, ah, certo, é verdade mesmo. Tá bom.”
Então recebo um e-mail. Recebo um e-mail de… Na verdade, eu pulei uma parte da história aqui. Então, a Gemma me mandou um e-mail e disse: “Aaron, vou te apresentar a essa
pessoa, o Neev, e rolou algum tipo de ataque muito grave ao RubyGems, e eles querem conversar com alguém sobre isso.” E eu fiquei tipo, “é, claro, tudo bem, quer dizer, posso conversar com você sobre isso”. E aí comecei a entrar em pânico porque percebi que não sou bom em checar meus e-mails. Então fiquei pensando: “Será que perdi algum e-mail? Tem alguma coisa no HackerOne que eu não vi? Tipo, o que eu fiz?”
Então, eu fico procurando freneticamente nos meus e-mails. Ah, não. Procurando por alguma coisa e não tem nada lá. Aí eu respondo pra pessoa. Eu digo: “Sinto muito, muito mesmo. Devo ter perdido seu e-mail ou algo assim, mas não consigo encontrá-lo de jeito nenhum.”
Essa pessoa me respondeu dizendo que não, que deve ter havido um engano. Isso não é um problema atual do RubyGems. Mas tem alguém, um pesquisador de segurança, tentando entrar em contato com alguém do RubyGems. E eu estou usando meus contatos para entrar em contato com você. E isso é, sei lá, interessante. Então eu pensei: “Tudo bem, tá bom”.
Recebi um e-mail de uma pessoa chamada Sydney von Arx me dizendo que os agentes da OpenAI tinham tentado explorar essa vulnerabilidade do RubyGems, a do cache. Ela colocou o link. E parece que esses quatro pacotes enviados tentaram explorar a vulnerabilidade para roubar as chaves de API dos usuários.
E eu pensei: “Isso é uma grande besteira. Qual é! Você não sabe disso.” Mas ela me mandou uns links. E eu fico tipo: “Tá bom. Bem, sabe, tô tentando ser uma pessoa responsável. Vou clicar em todos esses links e ler tudo.”
Meludite lendo o código. Enfim, então eu cliquei nos links e li o conteúdo. E vou mostrar pra vocês: aqui está um trecho de uma das pérolas que ela me enviou. Não é o texto completo, mas fiquem à vontade para escanear o código QR ali. Ele leva ao texto completo, então vocês podem dar uma olhada e ler, se quiserem.
E vou destacar algumas coisas aqui, neste código. A primeira é o comentário no início que diz “leaked_keys variants”. E eu começo, tipo, a suar na cadeira, e penso: “Nossa, isso não é nada bom”. E dá pra ver que está dentro desse script chamado script.rb. Além disso, o nome da gem é sln-leaker-5. Estranho. É mesmo.
A seguir, percebi que ele está fazendo uma solicitação GET ao RubyGems.org. E eu não coloquei o caminho aqui, mas é o caminho de autorização. É isso mesmo.
Então olhei para a parte de baixo e vi essa expressão regular específica, que vocês talvez tenham reconhecido na resposta, a resposta da chave de autenticação. E percebi o que isso estava fazendo e pensei: “Nossa, sim. Isso estava tentando invadir o RubyGems.org”. Então respondi a eles e disse: “Ah, tudo bem, sim. Isso é muito, muito interessante”.
E eles perguntaram se poderiam marcar uma reunião, tipo, uma reunião comigo. E eu falei: “Claro, vamos nos encontrar e conversar sobre isso”. Então, marcamos uma reunião. É, ah, sim, esse sou eu. Percebendo o que está acontecendo aqui.
Estou conversando com eles e digo: “Ok, isso é muito interessante. Vocês me enviaram isso. Está, mais uma vez, no script.rb. Como isso está sendo executado em algum lugar? Tipo, como alguém executaria isso?” Não está no extconf. E ela me disse que estava sendo executado no rubydoc.info.
E eu fiquei tipo: “O quê?”. RubyDoc.info. Se você acessar o RubyDoc.info, verá que é apenas um servidor de documentação. Ele exibe a documentação de várias gems. Vamos ver. O que é isso, afinal?
Se você der uma olhada dentro da gem, verá que há um arquivo .yardopts. O arquivo .yardopts tem a seguinte aparência. E você verá que ele diz algo como “load ./script.rb”. E o que acontece é que, se você tiver a gem Yard instalada, ela instala um plugin do RubyGems. E então, se você instalar outra gem, ao instalá-la, a gem Yard irá procurar dentro da sua gem por um arquivo .yardopts. E, se encontrar um arquivo .yardopts, ela executará este código aqui. Então, ela executará isso.
E ela me disse que sempre que uma gem fosse enviada, vamos ver se eu tenho — sim. Então, o que estava acontecendo aqui é que um bot enviava uma gem para o RubyGems.org assim. O RubyGems.org enviaria então um webhook para o RubyDoc.info. Portanto, o RubyDoc.info não tem nenhuma relação — é completamente independente do RubyGems.org. Ele enviaria um webhook para o RubyDoc.info. O RubyDoc.info diria: “Ótimo, há uma nova gem publicada”. Em seguida, ele baixaria a gem. Então, ele baixava o arquivo, o executava e o abria dentro de um contêiner do Docker. O contêiner do Docker ainda tinha acesso à rede. Ele executava o script malicioso. O script malicioso então baixava dados do governo do Reino Unido. Em seguida, ele empacotava esses dados como uma gem. Isso faria com que a gem fosse enviada para o RubyGems.org. O RubyGems.org enviaria um webhook para o RubyDoc.info. O RubyDoc.info executaria scripts maliciosos. Ele baixaria dados do governo do Reino Unido, etc., etc., etc.
Então, isso era uma loucura. Para mim, foi uma loucura. Eu não conseguia acreditar. Todas essas peças se encaixam assim. Isso é loucura. Tipo, como diabos isso pode estar acontecendo?
Quero fazer uma espécie de revisão cronológica disso porque isso me deixou de queixo caído. Então, nos dias 11 e 12 de maio rolou a campanha do “gem stuffer”. E quanto a esses RubyGems, dá para verificar as datas de envio. São de 11 de maio. Então, tudo bem. O relatório do cache do RubyGems chegou em 6 de julho, e a pessoa que o enviou era totalmente independente da OpenAI. Ela não tinha qualquer tipo de vínculo com a empresa. Eles descobriram isso por conta própria. Então, esses bots já sabiam desse problema há, sei lá, um mês, ou dois meses, eu acho. E tudo isso aconteceu bem antes da violação de segurança da Hugging Face também. Por fim, em 12 de setembro, Sydney e sua equipe publicaram o rubyhack.ai, e eu recomendo que vocês leiam. É uma análise muito boa e aprofundada sobre esse ataque específico.
Então, o que me deixou apavorado é que os bots descobriram que o RubyGems.org envia um webhook para o RubyDoc.info. Eles descobriram que o Yard executa código arbitrário no RubyDoc. Bem, ele executa código arbitrário mesmo. Depois, descobriram que o rubydoc.info processa o YardDoc com conexão de rede e executa esse código. Também descobriram isso — pelo menos é o que eu suspeito. Você vai perceber, se der uma olhada em todas as gems e na campanha de “gem stuffing”, que nem todas têm esse código para baixar chaves. E acho que isso se deve ao fato de que, quando essas gems começaram a ser enviadas, o pessoal do RubyGems — a equipe do RubyGems.org — começou a desativar as contas e a impedir que elas fizessem login. E o bot pensou: “Nossa, preciso de uma maneira de fazer login quando não tiver credenciais. Então, vou usar essas credenciais.” E eles decidiram explorar uma vulnerabilidade 0-day no RubyGems.org. Isso é uma loucura pra mim. Eles descobriram essa máquina de Rube Goldberg.
Agora, a equipe do RubyGems.org — tem um final feliz. A equipe do RubyGems.org conseguiu resolver a situação. Tipo, eles resolveram tudo completamente, consertaram, arrumaram tudo, taparam os vazamentos. Pelo que sabemos, ninguém, absolutamente ninguém foi explorado. Portanto, não houve nenhum dano às pessoas da comunidade. Está tudo bem. Sim. Então, muito obrigado a todos eles.
Então, quero encerrar esse assunto por aqui. Sou um otimista em relação à IA. Uso a IA todos os dias para escrever código. Uso o tempo todo. Na verdade, quero mudar isso. Quero mudar um pouco isso. Sou apenas um otimista, não necessariamente um otimista em relação à IA. Acho que este é o melhor momento para se estar vivo e acredito de verdade que as coisas só vão melhorar. Mas o fato é que, tipo, eu não precisei tomar nenhum remédio para ser otimista. Na verdade, sou um homem de meia-idade e não quero tomar mais nenhum remédio. Tipo, eu tenho — ah, não. Tenho muitos deles, mas não acho que ser otimista em relação à IA ou ao nosso futuro signifique necessariamente que eu precise me render a ela.
Já ouvi o argumento de que a IA é apenas nosso próximo compilador. Você não precisa ler o código de máquina que seu compilador produz, certo? Então, por que você precisaria ler o código de máquina ou o código que sua IA produz? Eu consigo entender esse argumento, de certa forma. É verdade, você provavelmente não lê o código de máquina que seu compilador gera. Mas isso ocorre porque o compilador fez uma promessa a você, fez essa promessa a você: a regra “como se”. Seja qual for a ação que ele execute, o comportamento que você pode observar corresponde ao código que você escreveu. E tudo o que mostrei a você nesta apresentação ilustra o custo de manter essa promessa. Agora, sua IA não fez essa promessa a você. Não existe uma regra “como se” para o seu inglês. Portanto, não vou entregar meu destino à IA. Vou continuar lendo meu código, e espero que vocês também o façam.
E quero encerrar com este slide. Entra aí, perdedor, vamos programar. Obrigado.
Artigo publicado · Atualizado
