1 điểm bởi GN⁺ 10 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Anubis đang thiết kế để mở rộng cơ chế proof-of-work bảo vệ website vượt ra ngoài SHA-256, đồng thời để client và server cùng chạy cùng một logic kiểm tra WebAssembly
  • Để không loại trừ các môi trường tắt WebAssembly, dự án đã chuẩn bị đường biên dịch lại sang JavaScript, nhưng cách này chậm hơn WebAssembly và còn có thể chậm hơn nữa nếu JIT cũng bị tắt
  • Bản wasm2js trong các bản phân phối Linux đã cũ nên cho đầu ra khác với bản Homebrew, vì vậy để có build tái lập được, dự án đã đóng gói kèm wasm2js được build bằng wasi-sdk
  • Với build C/C++, __DATE__, __TIME__, wasm-opt trong $PATH, và thứ tự con trỏ trong mã xử lý ngoại lệ có thể làm đầu ra ở mức byte dao động dù đầu vào giống nhau
  • Bản triển khai cuối cùng dùng --no-wasm-opt, setarch --addr-no-randomize, xác minh SHA-256 riêng cho x86_64 và arm64, cùng kiểm tra rebuild trong CI để đảm bảo tính quyết định trong cùng kiến trúc

Proof-of-work WebAssembly của Anubis và đường thay thế bằng JavaScript

  • Anubis muốn thêm kiểm tra proof-of-work dựa trên WebAssembly để quản trị viên có thể dùng các cơ chế proof-of-work không phải SHA-256 để bảo vệ website
  • Mục tiêu cốt lõi là không phải triển khai riêng logic kiểm tra cho client và server mà chỉ định nghĩa ở một nơi duy nhất
    • Client và server cùng liên kết vào cùng một WebAssembly để chạy logic kiểm tra
    • Hướng tới cấu trúc có thể xác nhận hai bên hoạt động theo lockstep
  • Các client tắt WebAssembly cũng nằm trong phạm vi cân nhắc
    • Có ràng buộc là không muốn trên thực tế loại người dùng khỏi website
    • Anubis phải cân bằng giữa trải nghiệm người dùng, trải nghiệm quản trị viên và trải nghiệm nhà phát triển
  • Giải pháp vòng được chọn là biên dịch lại WebAssembly sang JavaScript
    • Lấy cảm hứng từ The Birth and Death of JavaScript
    • JavaScript tạo ra chậm hơn WebAssembly tương đương
    • Khi tắt WebAssembly, đôi khi JavaScript JIT cũng bị tắt theo nên có thể còn chậm hơn
    • Việc nó có hiệu quả hơn JavaScript hiện tại trên phần cứng cấu hình thấp hay không vẫn cần nghiên cứu thêm

Vì sao phải đóng gói kèm wasm2js

  • Công cụ cần thiết là wasm2js của dự án binaryen
  • wasm2js có sẵn dưới dạng gói trong các bản phân phối Linux, nhưng phiên bản distro đã cũ nên không thể tạo ra đầu ra giống với bản Homebrew trong môi trường phát triển
  • Build tái lập được đòi hỏi tính quyết định của đầu ra
    • Để người dùng và người đóng gói có thể tin cậy binary wasm2js được commit vào kho Anubis, họ phải có khả năng tự build cùng phiên bản và nhận được cùng một dãy byte
    • Nếu có thể, ngay cả trên máy của người khác cũng nên cho ra cùng một dãy byte
  • Vì vậy dự án đưa vào một bản sao wasm2js được build bằng wasi-sdk nhắm tới đích WebAssembly

Những điểm tính tái lập dễ bị phá vỡ trong build C/C++

  • Ngay cả khi đưa vào cùng byte nguồn và cùng đầu vào, đầu ra của trình biên dịch không phải lúc nào cũng là cùng một dãy byte
  • Trong C/C++, chỉ riêng các macro dựng sẵn như __DATE____TIME__ cũng có thể tạo ra đầu ra không quyết định
    • Ví dụ hello.cpp được viết để in ra ngày và giờ tại thời điểm build
    • Một lần build in ra Jun 18 2026 00:00:59, còn lần khác in ra Jun 18 2026 00:01:11
    • Byte mã nguồn giống nhau nhưng đầu ra của trình biên dịch lại khác nhau
  • Với trình biên dịch nhỏ thì về lý thuyết có thể quyết định, nhưng trình biên dịch thực tế có nhiều biến số phức tạp hơn

Vấn đề Clang âm thầm chạy wasm-opt trong $PATH

  • Ngoài wasm2js, binaryen còn có wasm-opt để tối ưu đầu ra của trình biên dịch WebAssembly
  • Clang gọi wasm-opt ra shell trong quá trình build
    • Bình thường đây là hành vi hợp lý để cải thiện hiệu năng
    • Nhưng trong trường hợp này, khác biệt phiên bản wasm-opt trong $PATH đã phá vỡ tính tái lập
  • wasm-opt trên DGX Spark là /usr/bin/wasm-opt phiên bản 108, còn wasm-opt Homebrew trên workstation là phiên bản 130
  • wasi-sdk và binaryen phụ thuộc vào WebAssembly Exceptions extension
    • Theo Can I use, 93.86% người dùng trình duyệt đang dùng engine hỗ trợ tính năng này
    • C++ là ngôn ngữ dùng ngoại lệ nhiều, nên xử lý ngoại lệ native của WebAssembly có thể giảm boilerplate
  • wasmtime và wazero cần bật hỗ trợ ngoại lệ một cách tường minh
    • Có thể truyền -W exceptions=y cho wasmtime
    • Còn wazero cần một runner harness tùy biến
  • wasm-opt cũ trên máy arm đã thoát khi gặp lệnh xử lý ngoại lệ, làm build thất bại
  • Bằng cách truyền --no-wasm-opt ở bước liên kết, dự án đã loại bỏ đường gây mất tính tái lập này

Tác động của bố trí địa chỉ tới việc sinh mã xử lý ngoại lệ

  • Phiên bản Clang đang dùng cho thấy việc sinh mã nhạy với địa chỉ trong nhánh xử lý ngoại lệ của quá trình biên dịch wasm2js
  • Giá trị con trỏ thô ảnh hưởng tới thứ tự đầu ra của một số khối try_table
    • Mỗi lần build có thể chênh khoảng 29 byte
    • Tính toán gần như giống nhau, nhưng thứ tự byte thay đổi và tham chiếu catch cũng thay đổi
  • Ngay cả khi build cùng phiên bản cố định của wasm2js trên máy arm64, thứ tự lặp con trỏ vẫn khác với workstation nên cùng vấn đề lại xuất hiện
  • Có hai cách vòng
    • Dùng setarch --addr-no-randomize để tắt ngẫu nhiên hóa không gian địa chỉ cho lần build đó
    • Tạo checksum SHA-256 known-good riêng cho x86_64 và arm64 trên các máy đáng tin cậy
  • CI chạy ./build.sh trong ./utils/wasm/wasm2js rồi xác minh checksum
    • Nếu khớp shasums.x86_64 thì được tính là vượt qua checksum x86_64
    • Nếu khớp shasums.arm64 thì được tính là vượt qua checksum arm64
    • Nếu không khớp cả hai, nó sẽ in ra SHA-256 của wasm-opt_130.wasmwasm2js_130.wasm rồi thất bại
  • Tác vụ CI này chạy trên cả host x86_64 và arm64
  • Tính tái lập trên toàn bộ các host vẫn chưa đạt được, và vấn đề đó hiện vẫn là một bug upstream của LLVM
  • Ở trạng thái hiện tại, ít nhất bản build vẫn hoạt động một cách quyết định trong cùng kiến trúc

1 bình luận

 
Ý kiến trên Lobste.rs
  • Đây là lần đầu tôi biết clang lén chạy wasm-opt trong $PATH, và tôi thấy điều đó thật vô lý
    Vì vậy tôi đã kiểm tra xem điều này có ảnh hưởng đến zig cc không, và may mắn là không, vì nó chỉ chạy khi dùng clang làm trình điều khiển liên kết
    Nếu clang quyết định thứ tự dựa vào cách bố trí địa chỉ thì cá nhân tôi xem đó là một lỗi, và nếu vẫn tái hiện được ở bản phát hành mới nhất thì chắc tôi sẽ báo như vậy

    • Xe nói sẽ báo cáo upstream ở nơi khác, và đây rõ ràng là lỗi tính quyết định của LLVM
      Những nỗ lực để loại bỏ kiểu vấn đề này đã kéo dài suốt nhiều năm
    • Nếu thử dùng clang.exe trên Windows như một trình biên dịch chéo cho ổn định thì còn phát điên hơn nữa
      clang có khoảng 500 cách để giả định rằng nó sẽ được build cho hệ thống native
  • Không phải để chỉ trích, tôi tôn trọng việc đây là mã nguồn mở và OP đang cung cấp một dịch vụ phổ biến miễn phí
    Nhưng tôi thực sự ghét việc web đang biến thành thế này. Giờ đây chuyện vào một website rồi thấy ngay trang tải Anubis lóe lên đã trở nên quá phổ biến, và tôi không biết liệu chúng ta có thực sự muốn một web nơi mọi trang nổi tiếng đều hiện màn hình splash proof-of-work hay không
    Tôi cũng không biết còn lựa chọn nào khác khi crawler AI cứ tiếp tục kéo đến, nhưng tôi nghi ngờ liệu có bằng chứng nào cho thấy proof-of-work thực sự ngăn được crawler AI không. Chúng có nguồn vốn khổng lồ, và vốn dĩ đã bỏ ra lượng tính toán lớn hơn nhiều chỉ để đọc trang, nên chi phí giải proof-of-work có vẻ chỉ là một phần rất nhỏ

    • Có bằng chứng cho thấy proof-of-work ngăn được crawler AI. Ở đây cũng đã có nhiều bài viết về việc đó
      Trong thử nghiệm Anubis, nó rõ ràng là một biện pháp răn đe hiệu quả với lưu lượng không mong muốn, và chỉ với các quy tắc gần như mặc định đã liên tục chặn khoảng 90% yêu cầu của ba ứng dụng. DDR là 71.0%, ArcLight là 94.6%, Catalog là 92.4%
      Ngày 30 tháng 5 lưu lượng bot tăng vọt, khiến Catalog gần như không thể phục vụ cho đến khi áp dụng Anubis vào ngày 3 tháng 6; ở đỉnh điểm ngày 1 tháng 6, con số tăng lên 3,4 triệu HTTP request từ 2,1 triệu IP duy nhất, và thời gian tải trang vượt quá 70 giây. Sau khi áp dụng Anubis vào ngày 4 tháng 6, dịch vụ lại phục vụ được người dùng; tổng số request ứng dụng xử lý là 125 nghìn và thời gian tải trang được cải thiện còn 2,12 giây
      https://lobste.rs/s/ncyfcp/anubis_pilot_project_report_june_2025
      Trong một trường hợp khác, vấn đề cũng được giải quyết ngay sau khi triển khai Anubis; họ có thể thấy chính xác thời điểm đó trên hệ thống giám sát, và sau đó không còn một cảnh báo nào nữa. Cuộc tấn công vẫn tiếp diễn nhưng tải máy chủ ở mức thấp nhất, và theo tôi Anubis không chỉ chặn AI scraper mà còn hoạt động như bảo vệ DDoS
      https://lobste.rs/s/67ijih/day_anubis_saved_our_websites_from_ddos
    • Không nhất thiết phải dùng proof-of-work. Tôi đã triển khai một bộ chặn stateless không cần JS trên https://shithub.us, và để scraper vượt qua thì có lẽ cũng tốn kém ngang với proof-of-work đơn giản
      https://orib.dev/tmp/bandwidth.png
    • Không ai thực sự biết crawler là gì và dùng để làm gì, nhưng có vẻ nhiều crawler khá lười và xử lý những thứ khác thường không tốt
      Có người chỉ chặn được chúng bằng meta refresh hoặc một nút phải bấm. Vì thế Anubis có tác dụng, nhưng điểm cốt lõi không phải là bản thân proof-of-work mà là hành vi ngoài dự kiến
    • Chắc chắn tôi không muốn kiểu web như thế này
      Nó còn khổ sở hơn cả thời dùng web bằng trình duyệt tắt JavaScript. Tôi chỉ muốn web đơn thuần là các tài liệu, nhưng giờ ở đâu cũng phải đi qua Cloudflare, Anubis và các cổng captcha
    • Ngay từ đầu người ta đã biết rằng bất kỳ APT nào cũng có thể tăng tốc việc tính toán phản hồi Anubis. Ngay cả bản proof of concept năm ngoái cũng viết như vậy
      Rằng bot rồi sẽ luôn tìm được cách vượt WAF, còn người dùng thật thì lãng phí chu kỳ CPU ở màn hình tải
  • Đáng tiếc nhưng không có gì ngạc nhiên. Toolchain compiler có một lịch sử rất dài dựa vào những phụ thuộc ngầm vô lý kiểu “ngữ cảnh cục bộ phải tự nhiên mà đúng”
    Tuy vậy LLVM lại là phía đã đi đầu trong việc loại bỏ các phụ thuộc như thế, nên thấy điều này ở clang có phần lạ lùng. Nhờ đó mà chẳng hạn compiler Rust có thể tồn tại mà không cần khái niệm trình biên dịch chéo riêng biệt
    Nếu thử bootstrap một OS mà không dựa vào các build tool sẵn có thì điều đó hiện ra ngay. Việc tạo kernel, tạo libc và compiler cho kernel đó rồi chạy chúng, sau đó build lại toàn bộ trên OS mới là một công việc phi lý, cực kỳ phức tạp và nhạy cảm, đầy những giả định ngầm
    Đây là kiểu vấn đề hiếm hoi chủ yếu chỉ chạm tới các nhà phát triển OS và compiler, nên gần như không có công cụ tốt hay thực tiễn tốt nhất nào; với mỗi cặp compiler+OS có lẽ chỉ khoảng 5 người trên thế giới thực sự hiểu toàn bộ mọi thứ

    • Tôi ngạc nhiên khi nghe điều này. Tôi cứ nghĩ LLVM, khác với GCC, được thiết kế có tính đến biên dịch chéo, bằng cách trừu tượng hóa host và target
      Tôi cũng nghĩ toolchain Zig lấy được một phần khả năng đó từ LLVM, dù tất nhiên tôi hiểu họ đã làm rất nhiều để tách bạch gọn gàng hơn. Giờ tôi còn tự hỏi liệu họ có còn dùng LLVM nữa không
      Nhưng nếu clang cũng có cùng vấn đề, thì có lẽ LLVM đã không truyền lại một kiến trúc sạch sẽ hơn như tôi tưởng
  • Tôi biết bạn dùng Nix, nên tôi thắc mắc vì sao bạn không nhắc tới hoặc dùng Nix để giảm bớt ít nhất một phần biến động của môi trường
    Ví dụ vấn đề wasm-opt trong $PATH có vẻ như đã có thể được giảm nhẹ bằng Nix, hay là bạn đã dùng rồi mà tôi bỏ sót?

  • Ban đầu tôi ngây thơ nghĩ rằng việc chuyển wasm sang asm.js sẽ “dễ” thôi, nhưng hôm nay học được điều mới

    • Tôi cũng từng nghĩ vậy. Tiếc là trên thực tế nó phức tạp hơn nhiều so với tưởng tượng
  • Tiêu đề blog có vẻ câu click, nhưng nội dung thì hay
    Tôi thực sự ghét kiểu câu click