2 pontos por GN⁺ 2024-09-15 | 1 comentários | Compartilhar no WhatsApp
  • FlowTracker é um agente Java que rastreia como programas Java leem, manipulam e escrevem dados, mostrando a conexão entre a saída e as entradas, arquivos, rede ou constantes de código de onde ela veio
  • Ao observar o programa em execução, ele exibe I/O de arquivos e rede e, em especial, rastreia a relação entre entrada e saída, ajudando a entender o que a saída de um programa Java significa e por que foi gerada
  • Na demonstração com o Spring PetClinic, é possível acompanhar desde cabeçalhos de respostas HTTP, templates Thymeleaf, valores do banco de dados e até scripts SQL de inserção, explorando várias camadas da pilha de software
  • Internamente, ele instrumenta bytecode no momento do carregamento da JVM e combina hooks de métodos do JDK, análise de fluxo de dados, rastreamento de chamadas baseado em ThreadLocal e ClassOriginTracker para manter um mapeamento de origem centrado em strings, caracteres e bytes
  • No estado atual, está mais próximo de uma prova de conceito do que de algo pronto para produção; funcionou bem em alguns programas de exemplo, mas não serve para todos os programas e adiciona uma grande sobrecarga, deixando a execução bem mais lenta

O que o FlowTracker rastreia

  • FlowTracker é um agente Java que rastreia como os dados são lidos, transmitidos, transformados e escritos dentro de programas Java
  • Ele não se limita a mostrar I/O de arquivos e rede, mas também conecta a saída do programa às entradas de onde ela veio
  • O objetivo é entender o que a saída de um programa Java significa e por que o programa escreveu essa saída
  • O projeto atual é uma proof-of-concept que explora que tipo de insight pode ser obtido ao observar o comportamento de programas por essa perspectiva

Demo: rastreando a origem de respostas HTTP no Spring PetClinic

  • FlowTracker PetClinic demo permite ver no navegador como o Spring PetClinic processa requisições HTTP e gera páginas HTML com base em templates e dados do banco de dados
  • A tela mostra a resposta HTTP enviada pelo PetClinic pela rede, e ao clicar em uma parte do corpo da resposta é possível ver, na visualização inferior, de onde aquela parte veio
  • É possível selecionar entradas/origens rastreadas ou saídas/sinks na árvore à esquerda ou no botão no canto inferior esquerdo em dispositivos móveis
  • Camada de processamento HTTP

    • Ao clicar em "HTTP/1.1" ou em um cabeçalho HTTP, é possível ver que essa parte da resposta foi gerada por classes Apache Coyote no pacote org.apache.coyote
    • O FlowTracker mostra qual código produziu qual saída
  • Camada de templates Thymeleaf

    • Ao clicar em nomes de tags HTML como "html" ou "head", é possível ver que essa parte do HTML veio do arquivo layout.html
    • Depois de clicar em layout.html, pressionar o botão colorido + na parte inferior faz com que todas as partes vindas desse arquivo sejam destacadas com a mesma cor
    • Ao rolar para baixo, é possível confirmar que parte da resposta veio de outro arquivo, ownerDetails.html
    • Ao clicar nos caracteres < ou >, é possível ver que eles foram escritos pela biblioteca de templates Thymeleaf
  • Origem dos valores do banco de dados

    • A tabela da página HTML inclui informações vindas do banco de dados
    • Ao clicar em George na tabela, o rastreamento vai além de mostrar que o valor veio do banco de dados e chega até o script SQL que inseriu esse valor originalmente
    • Nesta demo, o rastreamento até o script SQL é possível porque foi usado um banco de dados em memória, então o conteúdo do banco não saiu da JVM

Demo com MySQL e independência de framework

  • Ao executar a mesma demo do PetClinic com um banco de dados MySQL, os valores são rastreados até o ponto de conexão com o banco
  • Nesse caso, é possível ver a consulta SQL enviada anteriormente para criar o valor e os detalhes de como o driver MySQL JDBC se comunica com o banco de dados
  • FlowTracker PetClinic mysql demo também mostra que o FlowTracker intercepta o conteúdo descriptografado transmitido pela conexão SSL com o banco de dados
  • O Spring PetClinic é apenas um exemplo, e o FlowTracker não depende de nenhum framework ou biblioteca específica
  • javac demo mostra como o FlowTracker pode ajudar a entender o formato do arquivo class gerado e o bytecode dentro dele ao observar o compilador Java

Como usar e cuidados

  • Atualmente, o FlowTracker está mais próximo de uma proof-of-concept do que de algo pronto para produção
  • Ele funcionou bem em vários programas de exemplo, mas não há garantia de que funcione bem em todos os programas
  • Ele adiciona muita sobrecarga, então a execução do programa fica muito mais lenta
  • Procedimento de uso:
    • Baixe o agent jar flowtracker-*.jar na página de releases do Github
    • Adicione -javaagent:path/to/flowtracker.jar à linha de comando do Java
    • Para desativar algumas otimizações da JVM que atrapalham o FlowTracker, adicione também à linha de comando a saída de java -jar flowtracker.jar jvmopts
    • Por padrão, o FlowTracker inicia um servidor web na porta 8011, então basta abrir http://localhost:8011/ no navegador
  • Mais opções de configuração estão em USAGE.md

Funcionamento interno: instrumentação de bytecode e modelo Tracker

  • O FlowTracker é um agente de instrumentação que injeta código no class file, ou seja, no bytecode, quando a JVM carrega classes
  • O código injetado mantém o mapeamento entre os dados na memória e suas origens enquanto o programa lê, transmite e escreve dados
  • O foco do rastreamento está em dados textuais e binários como String, char e byte[], e não principalmente em números, dados estruturados ou dados calculados
  • Formas de uso:
    • Algumas chamadas de métodos do JDK são substituídas por chamadas às versões de método do FlowTracker
    • Código é injetado em pontos centrais do JDK para rastrear entradas e saídas
    • Análise de fluxo de dados e instrumentação mais profunda são aplicadas para rastrear variáveis locais e valores na pilha dentro dos métodos
    • Código é adicionado antes e depois das chamadas de método, e no início e fim dos métodos chamados, para rastrear argumentos e valores de retorno com ThreadLocal
  • Modelo de dados Tracker

    • Tracker: armazena o conteúdo do objeto rastreado e informações sobre sua origem
    • content: dados como todos os bytes que passaram por um InputStream ou OutputStream
    • source: conecta um intervalo específico do conteúdo a um intervalo específico de outro tracker
    • TrackerRepository: mantém um grande Map<Object, Tracker> global que associa objetos de interesse ao respectivo Tracker
    • TrackerPoint: aponta para uma posição dentro de um tracker e representa um único valor primitivo, como a origem de um byte

Instrumentação básica: hooks do JDK e ASM

  • O FlowTracker mantém o Tracker atualizado inserindo chamadas a métodos de hook quando determinados métodos do JDK são chamados
  • O exemplo mais simples é System.arraycopy
    • Substitui a chamada a java.lang.System.arraycopy por uma chamada a com.coekie.flowtracker.hook.SystemHook.arraycopy
    • O SystemHook chama o arraycopy real e, em seguida, busca no TrackerRepository os trackers dos arrays de origem e destino, atualizando o tracker de destino para apontar para a origem
  • Para essa instrumentação, usa a biblioteca de manipulação de bytecode ASM
  • A maioria dos hooks é adicionada no lado chamado, dentro do método do JDK, e não no lado do chamador
    • Por exemplo, adiciona uma chamada a FileInputStreamHook.afterReadByteArray ao final de FileInputStream.read(byte[])
    • Essa instrumentação é implementada com um microframework próprio baseado em anotações que usa o AdviceAdapter do ASM
  • O FlowTracker adiciona hooks a classes do JDK relacionadas a I/O, como java.io.FileInputStream, java.io.FileOutputStream, sun.nio.ch.FileChannelImpl, sun.nio.ch.IOUtil e sun.nio.ch.NioSocketImpl
  • Implementação relacionada:

Rastreamento de valores primitivos e análise do fluxo de dados dentro do método

  • Valores primitivos como byte não têm identidade como objetos, então não podem ser rastreados com segurança como chave de Map no TrackerRepository
  • O FlowTracker reescreve o código para armazenar separadamente, em variáveis locais dentro do método, a origem dos valores primitivos
  • Por exemplo, após byte b = x[1], obtém o tracker de b com ArrayHook.getElementTracker(x, 1) e, em y[2] = b, registra a origem no array de destino com ArrayHook.setElementTracker(y, 2, bTracker)
  • Para isso, o FlowTracker realiza interpretação simbólica sobre os recursos de análise do ASM
  • Em cada ponto do método, modela de onde vieram e para onde vão os valores das variáveis locais e da pilha
  • Implementação relacionada:
  • Nem todos os valores primitivos são rastreados; o foco está em byte e char, e int e long são tratados de forma mais limitada

Fluxo de dados entre chamadas de método

  • A análise apenas dentro do método não consegue lidar com casos em que valores primitivos fluem como argumentos e valores de retorno de outros métodos
  • O FlowTracker armazena no Invocation os PointTracker dos argumentos e do valor de retorno, e os coloca em um ThreadLocal imediatamente antes da chamada do método
  • No ponto inicial do método chamado, usa Invocation.start(...) para recuperar do ThreadLocal essas informações e poder usar a origem dos argumentos primitivos
  • Dessa forma, mesmo em casos como out.write(b), em que um valor primitivo é passado para um método, o tracker de value pode ser herdado dentro de write(byte value)
  • Implementação relacionada:

Tratar o próprio código como origem dos dados

  • As principais origens rastreadas pelo FlowTracker são E/S e valores vindos do próprio código
  • Os valores vindos do código incluem constantes primitivas e String, como 'a' e "abc"
  • Para essas constantes, é criado um ClassOriginTracker por classe, que armazena uma representação textual da classe e das referências às constantes
  • Quando uma constante é referenciada, o tracker desse valor passa a apontar para a posição dentro dessa representação textual
  • Esse modelo trata as constantes como se tivessem sido lidas da representação textual do código, tornando-o semelhante ao modelo de rastreamento de E/S
  • Por motivos de desempenho, usa-se ConstantDynamic (JEP 309) para que o método constantPoint não seja chamado a cada execução do método
  • Implementações relacionadas:

Tratamento de literais de String e limitações

  • Para literais de String, é criada uma nova cópia de String, e o byte[] dentro de String.value é ligado ao ClassOriginTracker
  • Uma instrução como String s = "abc"; é reescrita para algo na forma String s = StringHook.constantString("abc", 1234, 81);
  • Essa abordagem quebra a garantia de string interning normalmente fornecida pela JVM
    • Originalmente, todas as ocorrências da mesma constante String deveriam referenciar a mesma instância
    • Após a instrumentação, códigos que dependem dessa garantia podem falhar
  • O FlowTracker inclui alguns mecanismos para reduzir esse problema
    • Com ConstantDynamic, mesmo que o mesmo literal de String na mesma linha seja executado várias vezes, a mesma instância é retornada sempre
    • Algumas expressões stringA == stringB são reescritas para Objects.equals(stringA, stringB), para que, sob certos aspectos, pareçam a mesma instância
    • Em alguns pacotes, como java.lang.*, o rastreamento de literais de String é desativado
    • Esse comportamento pode ser configurado por breakStringInterning em USAGE.md
  • Implementações relacionadas:

Fallback para valores não rastreados

  • O FlowTracker não rastreia todos os valores do programa
  • Os motivos incluem preocupações de desempenho, partes ainda não implementadas, valores de baixa relevância e o fato de que representar valores gerados pela combinação de várias origens exigiria um modelo de dados mais complexo
  • Quando um valor que não era rastreado chega a um ponto em que precisa passar a ser rastreado, ele é ligado ao ClassOriginTracker, de forma semelhante às constantes, e essa posição é representada como "<?>"
  • Por exemplo, como o comprimento de arrays não é rastreado, quando write(array.length) é chamado, a Invocation recebe um PointTracker que aponta para a posição do código onde write é chamado
  • Como resultado, mesmo que não seja possível ver a origem original em saídas de formato binário, em alguns casos ainda dá para interpretar rapidamente o significado do valor por meio das strings rastreadas ao redor e da posição no código

Outros tópicos de implementação que podem ser abordados

  • MergedValue lida com o rastreamento de valores que passam por ramificações e loops, sendo considerado uma das partes mais difíceis da análise de fluxo de dados
  • A concatenação de strings é tratada adicionando um hook ao MethodHandle retornado por StringConcatFactory via indification (JEP 280)
  • A implementação também inclui localizar o código-fonte, descompilar com Vineflower e conectar o bytecode às linhas do código-fonte
  • A configuração do ClassLoader é focada em evitar dependências de bootclasspath e conflitos com a aplicação, além de manter um ciclo de desenvolvimento rápido sem shading nem nested jar
  • O rastreamento de valores primitivos armazenados em campos também faz parte da implementação
  • O frontend é composto por um servidor web baseado em Jetty e JAX-RS, além de uma interface web baseada em Svelte

1 comentários

 
GN⁺ 2024-09-15
Opiniões no Hacker News
  • Muito legal. Criei uma ferramenta para Clojure na mesma direção, chamada FlowStorm: http://www.flow-storm.org/
    Para a instrumentação, em vez de um agente de instrumentação, uso um fork do compilador oficial do Clojure e aproveito a característica do Clojure de permitir trocar facilmente o compilador durante o desenvolvimento para inserir bytecode adicional.
    O interessante nos registros de execução de programas Clojure é que a maioria dos valores é imutável, então dá para capturar snapshots mantendo apenas ponteiros.
    Como a demonstração do post original explora uma aplicação web, deixo também uma demo de depuração de uma aplicação web com o FlowStorm para quem tiver interesse: https://www.youtube.com/watch?v=h8AFpZkAwPo
    • Muito legal mesmo. Fiquei curioso sobre por que escolheram JavaFX. Depois de escolher JavaFX, chegaram a olhar o cljfx?
    • Legal. Fiquei curioso se você também gosta da abordagem de usar metadados de estruturas de dados para rastrear valores.
  • Realmente impressionante.
    Gosto de como as ferramentas do ecossistema Java/JVM são excelentes. A última vez que fiquei assim surpreso foi quando vi o jitwatch: https://github.com/AdoptOpenJDK/jitwatch
    O FlowTracker me lembra um pouco a análise de contaminação (taint analysis), que rastreia como entradas de usuário não confiáveis ou valores secretos fluem dentro do programa para evitar que vazem ou sejam usados sem validação.
    O termo de busca é “dynamic taint tracking/analysis”.
    https://github.com/gmu-swe/phosphor
    https://github.com/soot-oss/SootUp
    https://github.com/feliam/klee-taint
  • A demo que rastreia um elemento HTML de volta até a instrução SQL que adicionou aquele valor ao banco de dados é impressionante.
    Consigo perfeitamente imaginar ferramentas assim se tornando a primeira linha de defesa ao rastrear bugs.
    • Obrigado.
      Durante o desenvolvimento do FlowTracker, muito do trabalho veio de fazer o rastreamento funcionar em programas de exemplo específicos.
      Eu sabia o resultado desejado, mas era difícil prever quais mecanismos de baixo nível precisariam ser suportados para que um exemplo específico funcionasse, e isso frequentemente dependia de detalhes de implementação internos do JDK ou de bibliotecas por onde os dados passavam.
      Mas o elemento HTML ficar conectado ao script SQL que inseriu aqueles dados no DB não foi assim.
      Não foi algo que eu esperava nem projetei; simplesmente aconteceu, então também fiquei bastante surpreso e animado com o que mais essa abordagem pode permitir.
    • Pensando bem, se houvesse uma forma padrão de rastrear a origem e a veracidade dos dados, muitos problemas poderiam ter sido evitados, e muitas regras de negócio também teriam sido expressas com mais facilidade.
      Seria bom também ter uma forma de rastrear se os dados são temporários ou se precisam ser gravados novamente.
      Quanto mais pudermos descrever essas restrições logo no início, melhor.
  • Ainda não entendi completamente a visão geral nem as formas de uso, mas isso me lembra um ambiente Smalltalk em que tudo pode ser inspecionado.
    Em Smalltalk, tudo é objeto e mensagem, então dá para rastrear para trás e interagir.
  • Muito legal. O vídeo de demonstração também é bom, e parece definitivamente útil ao mergulhar em uma base de código desconhecida.
  • Alguns anos atrás, experimentei um conceito parecido[1]. Eu queria aplicar algo como source maps de JavaScript ao HTML.
    Não consegui reservar tempo para expandir mais, mas acho que ferramentas de desenvolvimento web se beneficiariam muito desse tipo de rastreamento de atribuição de stack completo.
    Dito isso, integrar uma solução dessas aos frameworks existentes parece um grande desafio.
    [1] HTML Source Maps - https://github.com/connorjclark/html-source-maps https://docs.google.com/document/d/19XYWiPL9h9vA6QcOrGV9Nfkr...
  • No bom sentido, isso me lembra a demo do Eve-lang em que, ao depurar um programa, você simplesmente perguntava “por que isto não está aqui?”. Excelente trabalho.
    https://www.youtube.com/watch?v=TWAMr72VaaU&t=164s e https://witheve.com/
  • Se minha memória não falha, havia um artigo sobre uma ferramenta parecida para encontrar dinamicamente injeção de SQL em programas Java. É a mesma ferramenta?
    • Não, provavelmente era outra ferramenta.
      Se expandirmos o que o FlowTracker faz, ele também pode encontrar SQL ou outras vulnerabilidades de injeção. Então é possível que a ferramenta de que você lembrou usasse uma abordagem semelhante.
  • Já imaginei rastrear dados pela internet afora. Por exemplo, de onde veio uma imagem e em qual CDN ela estava.
    Ou perguntas como: “o que esta string viu desde o momento em que foi criada até chegar à minha tela?”
    Isto parece um passo nessa direção.
  • Estou tentando rodar esta ferramenta no VSCode, junto com um projeto que quero entender.
    Agora preciso pausar por um momento, mas estou ansioso para fazê-la funcionar e brincar um pouco com ela.