Odio los compiladores
(xeiaso.net)- 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
wasm2jsen distribuciones Linux estaba desactualizada y producía una salida distinta a la de Homebrew, por lo que terminaron empaquetando unwasm2jscompilado con wasi-sdk para lograr builds reproducibles - En builds de C/C++,
__DATE__,__TIME__,wasm-opten$PATHy 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 wasm2jsexiste 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
wasm2jsque 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
- Si usuarios y empaquetadores van a confiar en el binario de
- Para eso incluyeron una copia de
wasm2jscompilada 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.cppestaba escrito para imprimir la fecha y hora del momento de compilación - Un build imprimió
Jun 18 2026 00:00:59y otro imprimióJun 18 2026 00:01:11 - Los bytes del código fuente eran iguales, pero la salida del compilador fue distinta
- El ejemplo
- 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 incluyewasm-opt, que optimiza la salida del compilador de WebAssembly - Clang ejecuta
wasm-optcomo proceso externo durante el build- Normalmente eso es razonable para mejorar el rendimiento
- Aquí, la diferencia de versión de
wasm-optpresente en$PATHrompía la reproducibilidad
- En la DGX Spark,
wasm-optera la version 108 en/usr/bin/wasm-opt, mientras que en la workstation había unwasm-optversion 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
- A wasmtime se le puede pasar
- En máquinas arm, un
wasm-optantiguo terminaba al encontrar instrucciones de manejo de excepciones y hacía fallar el build - Pasaron
--no-wasm-opten 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
wasm2jsen 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
- Desactivar la aleatorización del espacio de direcciones para ese build con
- En CI se ejecuta
./build.shdentro de./utils/wasm/wasm2jsy 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.wasmywasm2js_130.wasmy el job falla
- Si coincide con
- 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
clangejecuta a escondidaswasm-optdesde$PATH, y realmente me parece absurdo.Revisé si esto también afectaba a
zig cc, y por suerte solo se ejecuta cuando se usaclangcomo driver del enlazador, así que no aplica ahí.Si
clangestá 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.Se han hecho esfuerzos durante años para eliminar este tipo de problemas.
clang.exede 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.
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
https://orib.dev/tmp/bandwidth.png
Hay quienes los bloquean solo con una etiqueta
meta refresho 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.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.
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.
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-opten$PATHparecerí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.
El título del blog suena a clickbait, pero el contenido está bien.
De verdad odio el clickbait.