Lançado no HN: FlowTracker – rastreamento de fluxo de dados em programas Java
(github.com/coekie)- 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
ThreadLocaleClassOriginTrackerpara 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 pacoteorg.apache.coyote - O FlowTracker mostra qual código produziu qual saída
- Ao clicar em
-
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 arquivolayout.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
- Ao clicar em nomes de tags HTML como
-
Origem dos valores do banco de dados
- A tabela da página HTML inclui informações vindas do banco de dados
- Ao clicar em
Georgena 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-*.jarna 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
- Baixe o agent jar
- 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,charebyte[], 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 origemcontent: dados como todos os bytes que passaram por umInputStreamouOutputStreamsource: conecta um intervalo específico do conteúdo a um intervalo específico de outro trackerTrackerRepository: mantém um grandeMap<Object, Tracker>global que associa objetos de interesse ao respectivoTrackerTrackerPoint: aponta para uma posição dentro de um tracker e representa um único valor primitivo, como a origem de umbyte
Instrumentação básica: hooks do JDK e ASM
- O FlowTracker mantém o
Trackeratualizado 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.arraycopypor uma chamada acom.coekie.flowtracker.hook.SystemHook.arraycopy - O
SystemHookchama oarraycopyreal e, em seguida, busca noTrackerRepositoryos trackers dos arrays de origem e destino, atualizando o tracker de destino para apontar para a origem
- Substitui a chamada a
- 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.afterReadByteArrayao final deFileInputStream.read(byte[]) - Essa instrumentação é implementada com um microframework próprio baseado em anotações que usa o
AdviceAdapterdo ASM
- Por exemplo, adiciona uma chamada a
- 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.IOUtilesun.nio.ch.NioSocketImpl - Implementação relacionada:
Rastreamento de valores primitivos e análise do fluxo de dados dentro do método
- Valores primitivos como
bytenão têm identidade como objetos, então não podem ser rastreados com segurança como chave deMapnoTrackerRepository - 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 debcomArrayHook.getElementTracker(x, 1)e, emy[2] = b, registra a origem no array de destino comArrayHook.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:
- FlowValue e ArrayLoadValue
- MergedValue: lida com situações em que um valor pode vir de vários lugares devido ao fluxo de controle, como
ifouloop - FlowInterpreter: extensão de
Interpreterdo ASM que interpreta instruções de bytecode e cria oFlowValueapropriado - Store e ArrayStore
- FlowTransformer: conduz todo o processo de análise e instrumentação
- Nem todos os valores primitivos são rastreados; o foco está em
byteechar, eintelongsã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
InvocationosPointTrackerdos argumentos e do valor de retorno, e os coloca em umThreadLocalimediatamente antes da chamada do método - No ponto inicial do método chamado, usa
Invocation.start(...)para recuperar doThreadLocalessas 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 devaluepode ser herdado dentro dewrite(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
ClassOriginTrackerpor 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
constantPointnã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 deString, e obyte[]dentro deString.valueé ligado aoClassOriginTracker - Uma instrução como
String s = "abc";é reescrita para algo na formaString 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
Stringdeveriam referenciar a mesma instância - Após a instrumentação, códigos que dependem dessa garantia podem falhar
- Originalmente, todas as ocorrências da mesma constante
- O FlowTracker inclui alguns mecanismos para reduzir esse problema
- Com ConstantDynamic, mesmo que o mesmo literal de
Stringna mesma linha seja executado várias vezes, a mesma instância é retornada sempre - Algumas expressões
stringA == stringBsão reescritas paraObjects.equals(stringA, stringB), para que, sob certos aspectos, pareçam a mesma instância - Em alguns pacotes, como
java.lang.*, o rastreamento de literais deStringé desativado - Esse comportamento pode ser configurado por
breakStringInterningemUSAGE.md
- Com ConstantDynamic, mesmo que o mesmo literal de
- 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, aInvocationrecebe umPointTrackerque aponta para a posição do código ondewriteé 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
MethodHandleretornado porStringConcatFactoryvia 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
Opiniões no Hacker News
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
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
Consigo perfeitamente imaginar ferramentas assim se tornando a primeira linha de defesa ao rastrear bugs.
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.
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.
Em Smalltalk, tudo é objeto e mensagem, então dá para rastrear para trás e interagir.
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...
https://www.youtube.com/watch?v=TWAMr72VaaU&t=164s e https://witheve.com/
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.
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.
Agora preciso pausar por um momento, mas estou ansioso para fazê-la funcionar e brincar um pouco com ela.