1 puntos por GN⁺ 11 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Anubis está diseñando una lógica de verificación en WebAssembly compartida entre cliente y servidor, ampliando su prueba de trabajo para proteger sitios web más allá de SHA-256
  • Para no excluir entornos con WebAssembly desactivado, preparó una ruta de recompilación a JavaScript, aunque es más lenta que WebAssembly y puede volverse todavía más lenta si también se desactiva el JIT
  • La versión de wasm2js en distribuciones Linux estaba desactualizada y producía una salida distinta a la de Homebrew, por lo que terminaron empaquetando un wasm2js compilado con wasi-sdk para lograr builds reproducibles
  • En builds de C/C++, __DATE__, __TIME__, wasm-opt en $PATH y el orden de punteros en código de manejo de excepciones pueden hacer que la salida a nivel de bytes cambie incluso con la misma entrada
  • La implementación final asegura determinismo dentro de cada arquitectura usando --no-wasm-opt, setarch --addr-no-randomize, verificación SHA-256 por x86_64 y arm64, y comprobaciones de reconstrucción en CI

La prueba de trabajo en WebAssembly de Anubis y la ruta alternativa en JavaScript

  • Anubis quiere agregar una verificación de proof-of-work basada en WebAssembly para que los administradores puedan usar métodos de prueba de trabajo distintos de SHA-256 para proteger sus sitios web
  • El objetivo principal es no implementar la lógica de verificación por separado en cliente y servidor, sino definirla en un solo lugar
    • Cliente y servidor se conectan al mismo WebAssembly para ejecutar la lógica de verificación
    • La idea es orientar la arquitectura a comprobar que ambos lados funcionen en lockstep
  • También se contemplan clientes con WebAssembly desactivado
    • Existe la restricción de no querer excluir de facto a los usuarios del sitio web
    • Anubis debe equilibrar experiencia de usuario, experiencia del administrador y experiencia del desarrollador
  • La solución elegida fue recompilar WebAssembly a JavaScript
    • Está inspirada en The Birth and Death of JavaScript
    • El JavaScript resultante es más lento que un WebAssembly equivalente
    • En algunos casos, desactivar WebAssembly también desactiva el JIT de JavaScript, lo que puede hacerlo aún más lento
    • Hace falta más investigación para saber si en hardware de bajos recursos esto es más eficiente que el JavaScript existente

Por qué tuvieron que empaquetar wasm2js

  • La herramienta necesaria es wasm2js, del proyecto binaryen
  • wasm2js existe como paquete en distribuciones Linux, pero la versión de las distros era antigua y no lograba generar la misma salida que la versión de Homebrew usada en el entorno de desarrollo
  • Para lograr builds reproducibles, la determinación de la salida es indispensable
    • Si usuarios y empaquetadores van a confiar en el binario de wasm2js que se hace commit en el repositorio de Anubis, deben poder compilar la misma versión por su cuenta y obtener exactamente los mismos bytes
    • Idealmente, esos mismos bytes también deberían salir en máquinas de otras personas
  • Para eso incluyeron una copia de wasm2js compilada con wasi-sdk apuntando al objetivo WebAssembly

Dónde se rompe fácilmente la reproducibilidad en builds de C/C++

  • Incluso usando los mismos bytes de código fuente y la misma entrada, la salida del compilador no siempre termina siendo exactamente la misma a nivel de bytes
  • En C/C++, solo con macros integradas como __DATE__ y __TIME__ ya puede aparecer una salida no determinista
    • El ejemplo hello.cpp estaba escrito para imprimir la fecha y hora del momento de compilación
    • Un build imprimió Jun 18 2026 00:00:59 y otro imprimió Jun 18 2026 00:01:11
    • Los bytes del código fuente eran iguales, pero la salida del compilador fue distinta
  • En teoría, un compilador pequeño podría ser determinista, pero en compiladores reales hay muchas más variables complejas

El problema de que Clang ejecutara silenciosamente wasm-opt desde $PATH

  • Además de wasm2js, binaryen también incluye wasm-opt, que optimiza la salida del compilador de WebAssembly
  • Clang ejecuta wasm-opt como proceso externo durante el build
    • Normalmente eso es razonable para mejorar el rendimiento
    • Aquí, la diferencia de versión de wasm-opt presente en $PATH rompía la reproducibilidad
  • En la DGX Spark, wasm-opt era la version 108 en /usr/bin/wasm-opt, mientras que en la workstation había un wasm-opt version 130 instalado con Homebrew
  • wasi-sdk y binaryen dependen de la extensión WebAssembly Exceptions
    • Según Can I use, el 93.86% de los usuarios de navegadores usa motores que la soportan
    • C++ usa muchas excepciones, así que el manejo nativo de excepciones en WebAssembly puede reducir boilerplate
  • En wasmtime y wazero, el soporte de excepciones debe activarse explícitamente
    • A wasmtime se le puede pasar -W exceptions=y
    • En wazero hace falta un harness de ejecución personalizado
  • En máquinas arm, un wasm-opt antiguo terminaba al encontrar instrucciones de manejo de excepciones y hacía fallar el build
  • Pasaron --no-wasm-opt en la etapa de linkeo para eliminar esta ruta no reproducible

Cómo afectó la disposición de direcciones a la generación de código de manejo de excepciones

  • La versión de Clang en uso mostraba generación de código sensible a las direcciones en la ruta de manejo de excepciones durante la compilación de wasm2js
  • Los valores de punteros sin procesar influían en el orden de salida de algunos bloques try_table
    • En cada build aparecía una diferencia de unos 29 bytes
    • El cálculo era casi igual, pero cambiaba el orden de los bytes y también las referencias de catch
  • Incluso al compilar la misma versión fijada de wasm2js en una máquina arm64, el orden de iteración de punteros difería respecto de la workstation y producía el mismo problema
  • Hubo dos soluciones de contención
    • Desactivar la aleatorización del espacio de direcciones para ese build con setarch --addr-no-randomize
    • Generar checksums SHA-256 known-good para x86_64 y arm64 en máquinas de confianza
  • En CI se ejecuta ./build.sh dentro de ./utils/wasm/wasm2js y luego se verifica el checksum
    • Si coincide con shasums.x86_64, se considera aprobado el checksum de x86_64
    • Si coincide con shasums.arm64, se considera aprobado el checksum de arm64
    • Si no coincide con ninguno, se imprime el SHA-256 de wasm-opt_130.wasm y wasm2js_130.wasm y el job falla
  • Este trabajo de CI corre tanto en hosts x86_64 como arm64
  • La reproducibilidad entre hosts distintos todavía no está resuelta y el problema sigue como un bug upstream en LLVM
  • Por ahora, al menos dentro de cada arquitectura el build se comporta de forma determinista

1 comentarios

 
Opiniones en Lobste.rs
  • Es la primera vez que me entero de que clang ejecuta a escondidas wasm-opt desde $PATH, y realmente me parece absurdo.
    Revisé si esto también afectaba a zig cc, y por suerte solo se ejecuta cuando se usa clang como driver del enlazador, así que no aplica ahí.
    Si clang está determinando el orden en función de la disposición de las direcciones, personalmente lo consideraría un bug, y si todavía se reproduce en la versión más reciente, lo reportaría como tal.

    • Xe dijo que lo reportaría upstream en otro lado, y esto definitivamente es un bug de determinismo en LLVM.
      Se han hecho esfuerzos durante años para eliminar este tipo de problemas.
    • Intentar usar clang.exe de forma confiable como compilador cruzado en Windows es todavía más desesperante.
      Hay como 500 formas en las que clang asume que va a compilar para el sistema nativo.
  • No intento criticar; respeto que sea open source y que el OP ofrezca gratis un servicio popular.
    Aun así, de verdad odio que la web esté cambiando así. Se ha vuelto común entrar a un sitio y que te aparezca de golpe una pantalla de carga de Anubis; no sé si de verdad queremos una web donde cada sitio popular muestre una pantalla de prueba de trabajo.
    Tampoco sé cuál sería la alternativa, porque los crawlers de IA no dejan de llegar, pero también dudo que haya pruebas de que la prueba de trabajo realmente detenga a los crawlers de IA. Tienen muchísimo financiamiento y ya hacen bastante más cómputo para leer las páginas, así que el costo de resolver la prueba de trabajo parece mínimo.

    • Sí hay evidencia de que la prueba de trabajo frena a los crawlers de IA. Aquí mismo se han publicado varios artículos al respecto.
      En el piloto de Anubis, fue claramente un mecanismo disuasorio efectivo contra tráfico no deseado, y con reglas casi básicas siguió bloqueando cerca del 90% de las solicitudes a tres aplicaciones. DDR tuvo 71.0%, ArcLight 94.6% y Catalog 92.4%.
      El 30 de mayo hubo un aumento repentino de tráfico de bots y, hasta que se aplicó Anubis el 3 de junio, Catalog quedó prácticamente fuera de servicio. En el pico del 1 de junio, llegó a 3.4 millones de solicitudes HTTP desde 2.1 millones de IP únicas, y los tiempos de carga superaron los 70 segundos. Después de aplicar Anubis el 4 de junio, volvió a ser usable para los usuarios; el total de solicitudes procesadas por la aplicación fue de 125 mil y el tiempo de carga mejoró a 2.12 segundos.
      https://lobste.rs/s/ncyfcp/anubis_pilot_project_report_june_2025
      En otro caso también, el problema se resolvió justo después de desplegar Anubis, y en el monitoreo se podía ver el momento exacto; desde entonces no hubo ni una sola alerta. El ataque seguía en curso, pero la carga del servidor estaba en niveles mínimos, así que parece que Anubis funcionó no solo para bloquear scrapers de IA, sino también como protección DDoS.
      https://lobste.rs/s/67ijih/day_anubis_saved_our_websites_from_ddos
    • No necesariamente hace falta prueba de trabajo. En https://shithub.us desplegaron un bloqueador sin estado y sin JS, y probablemente evadirlo le costaría a un scraper al menos tanto como una simple prueba de trabajo.
      https://orib.dev/tmp/bandwidth.png
    • Nadie sabe exactamente qué son muchos crawlers ni para qué sirven, pero muchos son bastante flojos y no manejan bien comportamientos fuera de lo común.
      Hay quienes los bloquean solo con una etiqueta meta refresh o con un botón que hay que presionar. Por eso Anubis funciona, pero la clave no es la prueba de trabajo en sí, sino el comportamiento inesperado.
    • Definitivamente no quiero una web así.
      Se está volviendo más insoportable que cuando usábamos la web con el navegador sin JavaScript. Ojalá la web siguiera siendo simplemente centrada en documentos, pero ahora en todos lados hay que pasar por Cloudflare, Anubis y portales con captcha.
    • Desde el principio ya se sabía que cualquier APT podía acelerar el cálculo de la respuesta de Anubis. Incluso el proof of concept del año pasado lo decía claramente.
      La idea era que los bots siempre encontrarán una forma de saltarse el WAF, y que al final los usuarios reales terminan desperdiciando ciclos de CPU en la pantalla de carga.
  • Es lamentable, pero no sorprende. Las toolchains de compiladores tienen una historia larguísima de depender de supuestos implícitos absurdos del tipo “el contexto local simplemente tiene que ser correcto”.
    Aun así, LLVM ha sido justamente uno de los proyectos que más ha liderado la eliminación de esas dependencias, así que resulta curioso ver algo así en clang. Gracias a eso, por ejemplo, el compilador de Rust pudo existir sin una noción separada de compilador cruzado.
    Si intentas bootstrapear un sistema operativo sin apoyarte en herramientas de build existentes, se vuelve evidente enseguida. Crear un kernel, luego una libc y un compilador para ese kernel, ejecutarlos y después recompilarlo todo sobre el nuevo sistema operativo es un proceso ridículamente complejo y delicado, lleno de supuestos implícitos.
    Como es un problema raro que casi solo les toca a desarrolladores de SO y compiladores, casi no hay buenas herramientas ni mejores prácticas, y da la impresión de que para cada combinación compilador+SO hay unas cinco personas en todo el mundo que realmente entienden el panorama completo.

    • Esto sí me sorprende. Yo pensaba que LLVM, a diferencia de GCC, estaba diseñado pensando en compilación cruzada, con abstracciones entre host y target.
      También creía que la toolchain de Zig obtenía parte de esa capacidad de LLVM, aunque claro, entiendo que Zig haya hecho mucho trabajo para separarlo todo de forma más limpia. Incluso me pregunto si ahora ya no usan LLVM.
      Pero si clang también tiene el mismo problema, entonces tal vez LLVM no heredó una arquitectura tan limpia como parecía.
  • Entiendo que usas Nix, así que me pregunto por qué no se mencionó ni se usó Nix para reducir хотя sea una parte de la variabilidad del entorno.
    Por ejemplo, algo como el problema de wasm-opt en $PATH parecería mitigable con Nix; ¿sí lo usaste y se me pasó?

  • Yo, ingenuamente, pensaba que convertir wasm a asm.js sería “fácil”, pero hoy aprendí algo nuevo.

    • Yo también lo pensaba. Lamentablemente, en la práctica es mucho más complejo de lo que uno imagina.
  • El título del blog suena a clickbait, pero el contenido está bien.
    De verdad odio el clickbait.