- FlowTracker es un agente de Java que rastrea cómo un programa Java lee, manipula y escribe datos, y muestra de qué entrada, archivo, red o constante de código proviene la salida.
- Observa el programa en ejecución para mostrar E/S de archivos y red y, en especial, rastrea la correspondencia entre entradas y salidas, lo que ayuda a entender qué significa la salida de un programa Java y por qué se generó.
- En la demo de Spring PetClinic, permite explorar varias capas de la pila de software, siguiendo desde los encabezados de una respuesta HTTP hasta las plantillas de Thymeleaf, valores de la base de datos y scripts SQL de inserción.
- Internamente, instrumenta bytecode cuando la JVM carga clases y combina hooks de métodos del JDK, análisis de flujo de datos, seguimiento de llamadas basado en
ThreadLocalyClassOriginTrackerpara mantener un mapeo de origen centrado en cadenas, caracteres y bytes. - Su estado actual está más cerca de una prueba de concepto que de estar listo para producción; aunque funcionó bien en algunos programas de ejemplo, no es adecuado para todos los programas y hace que la ejecución sea mucho más lenta debido a una gran sobrecarga.
Qué rastrea FlowTracker
- FlowTracker es un agente de Java que rastrea cómo se leen, se transmiten, se transforman y se escriben los datos dentro de un programa Java.
- No se limita a mostrar E/S de archivos y red: también conecta la salida del programa con las entradas de las que proviene.
- Su objetivo es ayudar a entender qué significa la salida de un programa Java y por qué el programa escribió esa salida.
- Actualmente, el proyecto es una proof-of-concept que explora qué perspectivas se pueden obtener al observar el comportamiento de un programa desde este punto de vista.
Demo: rastreo del origen de una respuesta HTTP en Spring PetClinic
- FlowTracker PetClinic demo permite ver en el navegador cómo Spring PetClinic procesa una solicitud HTTP y genera una página HTML a partir de plantillas y datos de la base de datos.
- En la pantalla se muestra la respuesta HTTP que PetClinic envió por la red; al hacer clic en una parte del cuerpo de la respuesta, se puede ver en la vista inferior de dónde proviene esa parte.
- En el árbol de la izquierda, o en el botón inferior izquierdo en móviles, se pueden seleccionar entradas y orígenes rastreados, o salidas y sinks.
-
Capa de procesamiento HTTP
- Al hacer clic en
"HTTP/1.1"o en un encabezado HTTP, se puede ver que esa parte de la respuesta fue generada por clases de Apache Coyote del paqueteorg.apache.coyote. - FlowTracker muestra qué código produjo qué salida.
- Al hacer clic en
-
Capa de plantillas Thymeleaf
- Al hacer clic en nombres de etiquetas HTML como
"html"o"head", se puede ver que esa parte del HTML provino del archivolayout.html. - Después de hacer clic en
layout.html, si se presiona el botón de color+en la parte inferior, todas las partes provenientes de ese archivo se muestran con el mismo color. - Al desplazarse hacia abajo, se puede comprobar que parte de la respuesta provino de otro archivo,
ownerDetails.html. - Al hacer clic en los caracteres
<o>, se puede ver que esos caracteres fueron escritos por la biblioteca de plantillas Thymeleaf.
- Al hacer clic en nombres de etiquetas HTML como
-
Origen de los valores de la base de datos
- La tabla de la página HTML incluye información proveniente de la base de datos.
- Al hacer clic en
Georgedentro de la tabla, no solo se ve que ese valor provino de la base de datos, sino que se rastrea hasta el script SQL que insertó originalmente el valor en la base de datos. - En esta demo se puede rastrear hasta el script SQL porque usa una base de datos en memoria, por lo que el contenido de la base de datos no salió de la JVM.
Demo de MySQL e independencia del framework
- Si se ejecuta la misma demo de PetClinic con una base de datos MySQL, los valores se rastrean hasta el punto de conexión con la base de datos.
- En este caso, se pueden ver la consulta SQL enviada previamente para crear los valores y los detalles de cómo el driver JDBC de MySQL se comunica con la base de datos.
- FlowTracker PetClinic mysql demo también muestra que FlowTracker intercepta el contenido descifrado transmitido a través de la conexión SSL con la base de datos.
- Spring PetClinic es solo un ejemplo; FlowTracker no depende de ningún framework ni biblioteca en particular.
- javac demo muestra cómo FlowTracker ayuda a observar el compilador de Java y entender el formato del archivo class generado y el bytecode que contiene.
Uso y advertencias
- Actualmente, FlowTracker está más cerca de una proof-of-concept que de estar listo para producción.
- Funcionó bien en varios programas de ejemplo, pero no se garantiza que funcione bien en todos los programas.
- Agrega mucha sobrecarga, por lo que la ejecución del programa se vuelve mucho más lenta.
- Procedimiento de uso:
- Descargar el jar del agente
flowtracker-*.jardesde las páginas de releases de Github. - Agregar
-javaagent:path/to/flowtracker.jara la línea de comandos de Java. - Agregar también a la línea de comandos la salida de
java -jar flowtracker.jar jvmoptspara desactivar algunas optimizaciones de la JVM que interfieren con FlowTracker. - De forma predeterminada, FlowTracker inicia un servidor web en el puerto 8011, así que basta con abrir
http://localhost:8011/en el navegador.
- Descargar el jar del agente
- Hay opciones de configuración más detalladas en
USAGE.md.
Funcionamiento interno: instrumentación de bytecode y modelo Tracker
- FlowTracker es un agente de instrumentación que inyecta código en los archivos class, es decir, en el bytecode, cuando la JVM carga las clases.
- El código inyectado mantiene en memoria el mapeo entre los datos y su origen mientras el programa lee, transmite y escribe datos.
- El foco del rastreo está en datos de texto y binarios como
String,charybyte[]; no se centra en números, datos estructurados ni datos calculados. - Cómo se usa:
- Sustituye algunas llamadas a métodos del JDK por llamadas a versiones de métodos de FlowTracker.
- Inyecta código en puntos clave del JDK para rastrear entradas y salidas.
- Realiza análisis de flujo de datos e instrumentación más profunda para rastrear variables locales y valores de la pila dentro de los métodos.
- Agrega código antes y después de llamadas a métodos, y al inicio y al final de los métodos llamados, para rastrear argumentos y valores de retorno con
ThreadLocal.
-
Modelo de datos Tracker
Tracker: conserva el contenido del objeto rastreado y su información de origen.content: datos como todos los bytes que pasaron por unInputStreamuOutputStream.source: conecta un rango específico del contenido con un rango específico de otro tracker.TrackerRepository: mantiene un granMap<Object, Tracker>global que asocia los objetos de interés con suTrackercorrespondiente.TrackerPoint: apunta a una posición dentro de un tracker y representa un único valor primitivo, como el origen de unbyte.
Instrumentación básica: hooks del JDK y ASM
- FlowTracker mantiene actualizado el
Trackerinsertando llamadas a métodos hook cuando se invocan ciertos métodos del JDK - El ejemplo más simple es
System.arraycopy- Reemplaza la llamada a
java.lang.System.arraycopypor una llamada acom.coekie.flowtracker.hook.SystemHook.arraycopy SystemHookllama alarraycopyreal y luego obtiene deTrackerRepositorylos trackers de los arreglos de origen y destino, y actualiza el tracker de destino para que apunte al origen
- Reemplaza la llamada a
- Para esta instrumentación usa la biblioteca ASM de manipulación de bytecode
- La mayoría de los hooks se agregan del lado del método llamado dentro del JDK, no del lado del llamador
- Por ejemplo, agrega una llamada a
FileInputStreamHook.afterReadByteArrayal final deFileInputStream.read(byte[]) - Esta instrumentación está implementada con un microframework propio basado en anotaciones que usa
AdviceAdapterde ASM
- Por ejemplo, agrega una llamada a
- FlowTracker agrega hooks a clases relacionadas con I/O del JDK, como
java.io.FileInputStream,java.io.FileOutputStream,sun.nio.ch.FileChannelImpl,sun.nio.ch.IOUtilysun.nio.ch.NioSocketImpl - Implementación relacionada:
Seguimiento de valores primitivos y análisis del flujo de datos dentro de métodos
- Los valores primitivos como
byteno tienen identidad como los objetos, por lo que no se pueden rastrear de forma segura como claves deMapenTrackerRepository - FlowTracker reescribe el código para almacenar por separado el origen de los valores primitivos en variables locales dentro del método
- Por ejemplo, después de
byte b = x[1], obtiene el tracker debconArrayHook.getElementTracker(x, 1)y, cuando ocurrey[2] = b, registra el origen en el arreglo de destino conArrayHook.setElementTracker(y, 2, bTracker) - Para ello, FlowTracker realiza interpretación simbólica (symbolic interpretation) sobre las capacidades de análisis de ASM
- Modela, en cada punto del método, de dónde vienen y hacia dónde van los valores de las variables locales y de la pila
- Implementación relacionada:
- FlowValue y ArrayLoadValue
- MergedValue: maneja situaciones en las que un valor puede provenir de varias ubicaciones debido al flujo de control, como sentencias if o loops
- FlowInterpreter: extensión de
Interpreterde ASM que interpreta instrucciones de bytecode y crea elFlowValueadecuado - Store y ArrayStore
- FlowTransformer: impulsa todo el proceso de análisis e instrumentación
- No rastrea todos los valores primitivos; se enfoca en
byteychar, y manejaintylongde forma más limitada
Flujo de datos entre llamadas a métodos
- Solo con el análisis interno de un método no se pueden manejar los casos en que valores primitivos fluyen hacia argumentos y valores de retorno de otros métodos
- FlowTracker almacena en
InvocationlosPointTrackerde los argumentos y del valor de retorno, y los coloca enThreadLocaljusto antes de la llamada al método - Al inicio del método llamado, se puede extraer la información de
ThreadLocalconInvocation.start(...)y usar el origen de los argumentos primitivos - Con este enfoque, incluso cuando se pasa un valor primitivo a un método, como en
out.write(b), el tracker devaluepuede heredarse dentro dewrite(byte value) - Implementación relacionada:
Tratar el propio código como fuente de datos
- Las principales fuentes que FlowTracker rastrea son los valores que provienen de I/O y del propio código
- Los valores que provienen del código incluyen constantes primitivas y
String, como'a'y"abc" - Para estas constantes se crea un
ClassOriginTrackerpor cada clase, y se conserva una representación textual de la clase y de las referencias a constantes - Cuando se referencia una constante, el tracker de ese valor pasa a apuntar a la posición dentro de esa representación textual
- Este modelo trata las constantes como si se hubieran leído desde la representación textual del código, por lo que se vuelve similar al modelo de rastreo de I/O
- Por rendimiento, se usa ConstantDynamic (JEP 309) para evitar que el método
constantPointse llame en cada ejecución de un método - Implementaciones relacionadas:
Manejo de literales String y limitaciones
- Un literal String crea una nueva copia de
Stringy vincula elbyte[]dentro deString.valueconClassOriginTracker - Una instrucción como
String s = "abc";se reescribe en la formaString s = StringHook.constantString("abc", 1234, 81); - Este enfoque rompe la garantía de interning de String que la JVM ofrece normalmente
- Originalmente, todas las apariciones de la misma constante String deben referenciar la misma instancia
- Después de la instrumentación, el código que depende de esta garantía puede romperse
- FlowTracker incluye varios mecanismos para reducir este problema
- Usa ConstantDynamic para devolver siempre la misma instancia aunque el mismo literal String de la misma línea se ejecute varias veces
- Reescribe algunas expresiones
stringA == stringBcomoObjects.equals(stringA, stringB), para que desde cierto punto de vista parezcan la misma instancia - Desactiva el rastreo de literales String en algunos paquetes, como
java.lang.* - Este comportamiento se puede configurar con
breakStringInterningenUSAGE.md
- Implementaciones relacionadas:
Fallback para valores no rastreados
- FlowTracker no rastrea todos los valores del programa
- Los motivos son las preocupaciones de rendimiento, partes que aún no están implementadas, valores de baja relevancia y el hecho de que representar valores generados por la combinación de varias fuentes requeriría un modelo de datos más complejo
- Cuando un valor que no estaba siendo rastreado llega a un punto en el que debe comenzar a rastrearse, se lo vincula a
ClassOriginTracker, de forma similar a una constante, y su posición se representa como"<?>" - Por ejemplo, como la longitud de un arreglo no se rastrea, cuando se llama a
write(array.length), aInvocationse le pasa unPointTrackerque apunta a la ubicación en el código del punto de llamada dewrite - Como resultado, aunque en la salida en formato binario no se pueda ver la fuente original, a veces es posible interpretar rápidamente el significado del valor a través de las cadenas rastreadas cercanas y la ubicación en el código
Más temas de implementación que se pueden abordar
- MergedValue maneja el seguimiento de valores que pasan por ramas y bucles, y se considera una de las partes más difíciles del análisis de flujo de datos
- La concatenación de strings se maneja mediante indification (JEP 280), agregando un hook al
MethodHandleque devuelveStringConcatFactory - La implementación también incluye buscar código fuente, decompilar con Vineflower y vincular bytecode con líneas de código fuente
- La configuración de ClassLoader se enfoca en evitar dependencias del bootclasspath y conflictos con la aplicación, y en mantener un ciclo de desarrollo rápido sin shading ni nested jar
- La implementación también incluye el seguimiento de valores primitive almacenados en campos
- El frontend está compuesto por un servidor web basado en Jetty y JAX-RS, y una UI web basada en Svelte
1 comentarios
Opiniones en Hacker News
Para la instrumentación, en vez de usar un agente de instrumentación, usa un fork del compilador oficial de Clojure y aprovecha una característica de Clojure que permite cambiar fácilmente el compilador durante el desarrollo para insertar bytecode adicional.
Algo interesante del registro de ejecución de programas en Clojure es que, como la mayoría de los valores son inmutables, se pueden capturar snapshots con solo conservar punteros.
Como la demo del post original explora una app web, dejo también una demo de depuración de una app web con FlowStorm para quien le interese: https://www.youtube.com/watch?v=h8AFpZkAwPo
Me encanta que las herramientas del ecosistema Java/JVM sean tan buenas. La última vez que me sorprendí así fue cuando vi jitwatch: https://github.com/AdoptOpenJDK/jitwatch
FlowTracker me recuerda un poco al análisis de contaminación: rastrear cómo fluyen dentro del programa las entradas de usuario no confiables o los valores secretos, para que no se filtren ni se usen sin validación.
La palabra clave para buscar es “dynamic taint tracking/analysis”.
https://github.com/gmu-swe/phosphor
https://github.com/soot-oss/SootUp
https://github.com/feliam/klee-taint
Es totalmente imaginable que, en el futuro, herramientas así se conviertan en la primera línea de defensa al rastrear bugs.
Al desarrollar FlowTracker, mucho del trabajo surgió de hacer que funcionara el rastreo para programas de ejemplo concretos.
Sabía cuál era el resultado objetivo, pero era difícil predecir qué mecanismos de bajo nivel había que soportar para que un ejemplo concreto funcionara, y con frecuencia dependía de detalles internos de implementación del JDK o de las bibliotecas por donde pasaban los datos.
Pero que un elemento HTML se conectara con el script SQL que insertó esos datos en la DB no fue así.
No fue algo que esperara ni que hubiera hecho intencionalmente: simplemente ocurrió, así que a mí también me sorprendió bastante y me dejó con ganas de ver qué más se puede hacer con este enfoque.
También sería bueno tener una forma de rastrear si los datos son temporales o si deben volver a escribirse.
Cuantas más de estas restricciones puedan declararse desde el principio, mejor.
En Smalltalk todo son objetos y mensajes, así que se puede rastrear hacia atrás e interactuar con ellos.
No llegué a dedicarle más tiempo para ampliarlo, pero creo que las herramientas de desarrollo web podrían beneficiarse mucho de este tipo de rastreo de atribución full stack.
Dicho eso, integrar soluciones así en frameworks existentes parece un gran desafío.
[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 y https://witheve.com/
Si se amplía lo que hace FlowTracker, también se pueden encontrar SQL u otras vulnerabilidades de inyección. Así que es posible que la herramienta que tienes en mente usara un enfoque parecido.
O preguntas como “¿qué vio esta cadena desde el momento en que se generó hasta que llegó a mi pantalla?”.
Esto parece un paso en esa dirección.
Por ahora tengo que hacer una pausa, pero me entusiasma hacerla funcionar y trastear un poco con ella.