2 puntos por GN⁺ 2024-09-15 | 1 comentarios | Compartir por WhatsApp
  • 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 ThreadLocal y ClassOriginTracker para 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 paquete org.apache.coyote.
    • FlowTracker muestra qué código produjo qué salida.
  • 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 archivo layout.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.
  • 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 George dentro 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-*.jar desde las páginas de releases de Github.
    • Agregar -javaagent:path/to/flowtracker.jar a la línea de comandos de Java.
    • Agregar también a la línea de comandos la salida de java -jar flowtracker.jar jvmopts para 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.
  • 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, char y byte[]; 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 un InputStream u OutputStream.
    • source: conecta un rango específico del contenido con un rango específico de otro tracker.
    • TrackerRepository: mantiene un gran Map<Object, Tracker> global que asocia los objetos de interés con su Tracker correspondiente.
    • TrackerPoint: apunta a una posición dentro de un tracker y representa un único valor primitivo, como el origen de un byte.

Instrumentación básica: hooks del JDK y ASM

  • FlowTracker mantiene actualizado el Tracker insertando 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.arraycopy por una llamada a com.coekie.flowtracker.hook.SystemHook.arraycopy
    • SystemHook llama al arraycopy real y luego obtiene de TrackerRepository los trackers de los arreglos de origen y destino, y actualiza el tracker de destino para que apunte al origen
  • 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.afterReadByteArray al final de FileInputStream.read(byte[])
    • Esta instrumentación está implementada con un microframework propio basado en anotaciones que usa AdviceAdapter de ASM
  • 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.IOUtil y sun.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 byte no tienen identidad como los objetos, por lo que no se pueden rastrear de forma segura como claves de Map en TrackerRepository
  • 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 de b con ArrayHook.getElementTracker(x, 1) y, cuando ocurre y[2] = b, registra el origen en el arreglo de destino con ArrayHook.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:
  • No rastrea todos los valores primitivos; se enfoca en byte y char, y maneja int y long de 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 Invocation los PointTracker de los argumentos y del valor de retorno, y los coloca en ThreadLocal justo antes de la llamada al método
  • Al inicio del método llamado, se puede extraer la información de ThreadLocal con Invocation.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 de value puede heredarse dentro de write(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 ClassOriginTracker por 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 constantPoint se 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 String y vincula el byte[] dentro de String.value con ClassOriginTracker
  • Una instrucción como String s = "abc"; se reescribe en la forma String 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 == stringB como Objects.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 breakStringInterning en USAGE.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), a Invocation se le pasa un PointTracker que apunta a la ubicación en el código del punto de llamada de write
  • 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 MethodHandle que devuelve StringConcatFactory
  • 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

 
GN⁺ 2024-09-15
Opiniones en Hacker News
  • Genial. Hice una herramienta para Clojure en la misma línea, FlowStorm: http://www.flow-storm.org/
    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
    • Muy bueno. Me da curiosidad por qué eligieron JavaFX. Después de elegir JavaFX, ¿también evaluaron cljfx?
    • Está bueno. Me pregunto si también te gusta el enfoque de usar metadatos de estructuras de datos para rastrear valores.
  • Realmente impresionante.
    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
  • La demo que rastrea un elemento HTML hacia atrás hasta la sentencia SQL que agregó ese valor a la base de datos es impresionante.
    Es totalmente imaginable que, en el futuro, herramientas así se conviertan en la primera línea de defensa al rastrear bugs.
    • Gracias.
      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.
    • Pensándolo bien, si hubiera existido una forma estándar de rastrear la procedencia y veracidad de los datos, se habrían prevenido muchísimos problemas y muchas reglas de negocio también serían más fáciles de expresar.
      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.
  • Todavía no entiendo del todo el panorama completo ni la forma de usarlo, pero me recuerda a un entorno Smalltalk donde se puede inspeccionar todo.
    En Smalltalk todo son objetos y mensajes, así que se puede rastrear hacia atrás e interactuar con ellos.
  • Muy bueno. El video de la demo también está bueno, y definitivamente parece útil para meterse en una base de código desconocida.
  • Hace algunos años experimenté con un concepto similar[1]. Quería aplicar algo como los source maps de JavaScript a HTML.
    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...
  • En el buen sentido, me recuerda a la demo de Eve-lang donde, al depurar un programa, simplemente preguntabas “¿por qué no está aquí?”. Excelente trabajo.
    https://www.youtube.com/watch?v=TWAMr72VaaU&t=164s y https://witheve.com/
  • Si no recuerdo mal, había un paper sobre una herramienta similar para encontrar dinámicamente inyección SQL en programas Java. ¿Es esta la misma herramienta?
    • No, probablemente era otra herramienta.
      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.
  • En algún momento imaginé rastrear datos a través de internet. Por ejemplo, de dónde venía una imagen y en qué CDN había estado.
    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.
  • Estoy intentando ejecutar esta herramienta en VSCode, junto con un proyecto que quiero entender.
    Por ahora tengo que hacer una pausa, pero me entusiasma hacerla funcionar y trastear un poco con ella.