Programming guides for beginner...
Any comments are welcomed....
I hope it helps!!! Thanks for drop by...
Showing posts with label compilers. Show all posts
Showing posts with label compilers. Show all posts

Friday, July 3, 2026

He Translated All of Rustc to C. So You Can Target a Z180.

A solo developer posted a GitHub repository on 2 July 2026 that contains a working build of the Rust compiler compiled from 46 million lines of C. The repository is FractalFir/crustc, and the working build was produced by cilly, a Rust-to-C compiler backend FractalFir has been writing in private for the last three years. The repository's own README is blunt about what this is: "This is a demo/teaser for my new Rust to C compiler toolchain... This repo just shows the compiler compiling itself, as I believe this is the flashiest showcase I could do." The bootstrap is the flashy part. The reason he wanted to do it in the first place is the part that actually matters. FractalFir wants to compile Rust for a Z180.

The why is a 1985 CPU

The Zilog Z180 is a Z80-compatible microcontroller from 1985 that still ships on modern embedded boards. The SDCC compiler is the only C compiler that targets it. SDCC has been around for thirty years, is the de-facto toolchain for the Z80/Z180/8051 family, and is not LLVM. There is no realistic path to a hosted LLVM Rust toolchain for a Z180. There never has been. The argument Rust-curious embedded engineers have been making for a decade is that, for the long tail of old microcontrollers that only have a C compiler, Rust is structurally locked out.

cilly is a wrapper around rustc that emits C, then hands the C to whatever C compiler you point it at. The toolchain config in the README is the part that makes the design concrete. The triple is sdcc_z180-unknown-none, the executable is /usr/bin/sdcc, the base args are -mz180 --std-c89 -c, and the input_arg_template is {input}. That is a one-target JSON config that says: take Rust source, emit C, hand it to SDCC, target a Z180. From the user's perspective, this looks like defining a target triple and a C compiler; the toolchain does the rest. It is the C backend LLVM never built and probably never will.

The compiler is the demo

crustc is the use case. The repository is 46 million lines of generated C that, when compiled with make -j20 LLVM_LIB_DIR=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib, produces a working rustc. The bootstrap timing is in the README: make -j20 937.98s user 123.77s system 1352% cpu 1:18.48 total — roughly 78 seconds of wall clock at 13.5x parallelism, no optimizations, on the developer's aarch64 workstation (Linux 6.17.0-1021-nvidia, GCC 13.3.0, Ubuntu 24.04). The produced binary is a real rustc:

$ LD_LIBRARY_PATH=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib:./rustc_driver ./rustc/rustc --version
rustc 1.98.0-nightly (c712ea946 2026-06-16)

That rustc is the version the project bootstrapped against (rustup install nightly-2026-06-16 per the README), and it can compile Rust programs once core/alloc/std are built for the new target. The bootstrap order is crustc → build core → build alloc → build std → use it like any other Rust toolchain. The hard part is the first step, and the hard part is done.

What cilly does that rustc_codegen_c did not

FractalFir has been working on this for three years. The README lists public attempts: rustc_codegen_clr, and "a lot of private ones" — fourteen total by his count, with cilly being the fourteenth. The failure mode of the previous attempts is in the README in a single sentence, and that sentence is the technical core of why cilly is different: "The main innovation behind cilly is that it adapts to C compilers."

The mechanism is concrete. cilly generates "witness" programs that probe the target C compiler for what it actually supports. The README includes the simplest example:

/* This compiles if and only if our C compiler supports _Thread_local. */
_Thread_local int KEYWORD_TLS_SUPPORTED;

If the witness compiles, cilly knows the compiler supports thread-local storage and can use it. If it does not, cilly falls back. Same pattern for type sizes, alignments, character encodings, and integer formats. The README: "All type layouts, sizes, alignments, character encodings (ASCII), and integer formats (two's complement) are queried for. With fallbacks, where possible." The hard part of any cross-compiler backend is not the language mapping; it is the assumption space. cilly queries the assumption space at build time and adapts.

The README's "stretch" example is the one that captures the philosophy:

/* This will pass in some C compilers. */
assert(sizeof(float) == sizeof(double));

The point is not that the assertion is right. The point is that the assertion is a probe, and cilly writes the probes, not the user. The C cilly emits is compiler-specific — you cannot take the C generated for ARM64 and run it on RISC-V32, because the C is the result of a particular C compiler's testimony about its own behavior. The output is not portable C. The output is portable Rust, with a C compiler as the substrate.

Network transparency is the structural trick

The part of the README that nobody else is writing about is the network transparency clause. "cilly is network transparent, and can talk to C compilers over TCP (may be extended to weird things like UART if need be). This is a solution to the bootstrap paradox / platforms without C cross compilers. You build a small C server on your Blorbo OS, run rustc on some normal platform like Linux, and let cilly talk over the wire."

This solves a real problem. For a platform that has no Rust compiler and no C cross-compiler, but does have a C compiler running natively, the only way to get a Rust program to run is to drive the native C compiler remotely. The README includes a transcript of this actually working — a "Hello, world!" compiled for Plan 9 from an ARM64 Linux host, with nm output showing a real rust_begin_unwind symbol mangled with the standard Rust scheme. The bootstrap chain is rustc on ARM64 Linux → cilly C output → TCP → Plan 9 SDCC-equivalent → Plan 9 binary. The TCP link is the missing piece in the previous attempts' toolchains.

The architectural shape is the same one that made rustc_codegen_c never go anywhere. The previous attempts assumed a cross compiler — a C compiler running on the host that can produce binaries for the target. The targets that don't have a cross compiler are exactly the targets that need this. The Plan 9 transcript is cilly working through a UART-equivalent in production, not a demo.

Where this is structurally weak

The README is honest about the limits, and the limits are real. The optimization path is the biggest one: "I strongly recommend not enabling optimizations: both because they may break stuff (this is just a demo, and it's... ee... rough around the edges) AND because optimizations take time at this scale. Without opts, my machine builds the project in a few minutes. With opts, expect to choke on some specific larger rust files." A 78-second debug build of rustc is a demonstration. A 78-second optimized build of rustc is what would make this a real product. The demo does not have it yet, and the difference between the two is what the next release has to close.

The ABI compatibility story is partial. "Generated code is mostly ABI compatible with normal rustc compiled code. I say mostly, because on some platforms (like arm64) rustc choose an ABI not representable from C." The specific failure is the sret struct return pointer convention; on most platforms you can synthesize it by passing the out pointer as the first argument, but on ARM64 the registers are different and the C compiler will not cooperate for small structs. There is a footnote-length discussion in the README about which struct sizes break, and the honest answer is "small structs, less than 16 bytes." That is a real portability cost.

The compiler has a known bug that FractalFir openly admits he cannot explain: "For some weird path-canonicalization-reasons, crustc can crash when run in the directory it was built in (repo root, crustc). Works fine in other places." He wrote a self-deprecating "I am confused too" in the README, and that is the right call. The bug is real, the fix is unknown, the release is still useful without it being fixed.

Finally, there is the personal cost. The README is a list of things the developer has had to deal with alongside writing a Rust compiler backend for three years. "I got a job (which means I no longer write code on a laptop with its G & C keys broken, yay!). I got uni (well I am on summer break, but thesis-s don't tend to write themselves). I put my left hand in a blender. The blender won. (Still have all my fingers, just some stitches). I will not elaborate further." Three years, on summer break, with a stitched-up hand, as a side project, written by a real person with a real life. That is what it took to ship a Rust compiler in 46 million lines of C, and that is the part of the README that no press release is going to capture.

The community response is the second-order story

The Hacker News thread for the project, submitted 2 July 2026 by Philpax as item 48768464, was at 321 points and 61 comments as of 3 July 2026 evening UTC+8 (HN Algolia API, per-item endpoint for 48768464 and search-index endpoint for crustc+rustc+c). The most useful signal is in the comments: Tiberium asked about performance ("this can be interesting even for non-porting reasons"). taris2 raised Diverse Double-Compiling as a verification technique (paraphrasing his comment): use crustc to compile the Rust source code and produce a second compiler; then use both that compiler and the official rustc, with deterministic flags, to compile the same Rust source code; the two output binaries should match bit for bit. This is a real, technically valid use of the project, and the fact that someone suggested it within hours of the repo going up is the signal that the audience is the right one.

ronsor picked up on the blender line ("What a shame. I would've read an article about this"), which tells you the community is reading the README, not just the headline. lioeters noticed the iteration count: "Gotta respect the dedication to a niche interest." Cadwhisker linked the landing page in the README — the three-year-old public plan that called out Z80/SDCC support as the goal. The conversation in the thread is about the design.

The structural argument

The argument that "Rust does not support obscure hardware" is the single most-deployed reason engineers pick C over Rust in embedded contexts. The Z180, the 8051, the PIC16, the AVR with a proprietary toolchain, the custom ASIC with a vendor-locked compiler — these are the platforms Rust cannot reach because the LLVM backend is the only one rustc ships, and the LLVM backend is not going to grow a Z180 target because no one at any company cares enough. The argument is structurally valid; "all the interesting embedded targets have LLVM" is the standard Rust-side reply, and it is true for the ARM Cortex-M family and false for the long tail.

cilly does not fix the long tail. It makes the long tail reachable from the Rust side, with a C compiler as the substrate. The C compiler does not have to be LLVM. The C compiler does not have to be modern. The C compiler has to exist. That is a meaningfully different proposal than "we need to port LLVM to every microcontroller" and it is the reason the project has any chance of going anywhere. The README makes the framing explicit: "The primary goal of this is support for old/obscure hardware with no LLVM/GCC support. There are still some systems out there that don't support Rust but support C." The Z180 is the demonstrable target on the landing page. The architecture, applied to every C compiler that exists, is the part that scales.

The cost is a developer willing to spend three years on a niche. FractalFir is the only one. The README's personal paragraph is not a joke; it is the answer to the question "why is this not funded?" The answer is "it is, by one person, on summer break, with a stitched-up hand." That is not a sustainable model. It is the only model that gets to this milestone, and the milestone is what makes the model worth copying.

What this means for you

If you write Rust for a living and you have ever been told "we can't use Rust on this project because the target doesn't have LLVM," this is your counterexample. The target did not get LLVM. The target got cilly and SDCC. The exact same architecture works for any C-compiler-only target. The cost is a build that is 50-100x slower than LLVM and an ABI that is "mostly" compatible. The benefit is Rust on hardware nobody else can put Rust on.

If you maintain a C compiler for a niche target, this is the moment to talk to the cilly author. Every C compiler is now a Rust compiler, with the cilly backend in front. The outreach cost is one email. The engineering cost is testing the witness programs for your compiler's specific behaviors and reporting back. SDCC is the first one wired up, with sdcc_z180-unknown-none as the example target. Your compiler is the next one, if you want to be the next one.

If you do security research, taris2's suggestion on the HN thread is the structural one. Diverse Double-Compiling with a cilly-bootstrapped compiler is now a real option for any platform with a C compiler. The threat model is "what if the official rustc has a backdoor?" and the answer is now "compile rustc itself with cilly, run both compilers on the same source with deterministic flags, and diff the output binaries bit for bit." This is not theoretical. The toolchain is here, the source code is on GitHub, and the taris2 recipe is the only thing between this and a runnable experiment.

If you write embedded firmware in C, the long tail of C-only targets is the queue you have already been working through for years. The cilly work means the queue is about to get a Rust column. The teams that have already shipped a Z180/8051/AVR-in-proprietary-C product are the ones who can show "we already know the platform, we are adding Rust alongside C" — and that is a different conversation to have with management than the teams that are still deciding whether to start a Rust evaluation. The people with the platform knowledge are about to be the bottleneck on the Rust side, and the lead time to develop that knowledge is measured in years.

What to do this week

# 1. Skim the README. The whole design is in the first 200 lines and
#    the 46M-line C is a build artifact, not a code review target.
#    https://github.com/FractalFir/crustc — start there.
#
# 2. Decide which of the four "what this means for you" buckets
#    above applies to you, and act on it. The four buckets are:
#    - You write Rust for a living: this is your counterexample
#      to the "no LLVM, no Rust" argument. Save the link.
#    - You maintain a C compiler: open an issue on the cilly
#      tracker and offer to test your compiler as a substrate.
#    - You do security research: file the DDC idea. The threat
#      model is real, the toolchain is here, the work is doable.
#    - You write embedded firmware in C: this is the deadline.
#      Open a tracking issue for "Rust on <your long-tail target>"
#      and put a date on it.
#
# 3. Try the build, if you have an aarch64 Linux host with ~16GB
#    of RAM and an hour. The deps are GCC, GNU make, and an
#    existing rustup nightly:
rustup install nightly-2026-06-16
git clone https://github.com/FractalFir/crustc
cd crustc
make -j20 LLVM_LIB_DIR=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib
LD_LIBRARY_PATH=~/.rustup/toolchains/nightly-2026-06-16-aarch64-unknown-linux-gnu/lib:./rustc_driver ./rustc/rustc --version
# Expected output: rustc 1.98.0-nightly (c712ea946 2026-06-16)
# If you get that, you have a Rust compiler in 46M lines of C
# running on your machine. Pin that in your memory.
#
# 4. Do not optimize the build. The README is explicit that
#    optimization is broken at this scale right now. The demo
#    is debug-mode, and the product will be debug-mode for a
#    while longer. The fact that 78 seconds of debug build of
#    rustc is a milestone is the point, not the limit.

cilly is a real project, the crustc demo is a real compiler, and the Z180 is a real target. The structural argument against Rust on the long tail of embedded targets has been answered, by one person, on summer break, with a stitched-up hand, as a side project, in three years. The answer is a C backend that adapts to whatever C compiler you have, the cost is performance and a partial ABI story, and the benefit is that the long tail of obscure hardware is no longer off-limits to Rust. The community response on HN is the second-order signal that the audience is the right one. This is the part of the post that is going to read the same in two years.

Related reads

  • Grit Brought Rust-Powered Git Search to AI Agents — Rust as the substrate for developer tools, applied to AI-agent workflows. The same bet Rust made for systems software is the bet cilly is making for compiler backends: the language is the leverage.
  • Speculative KV Coding Hit 4× Lossless Cache Compression — engineering as a niche pursuit that turns into a structural lever. The thesis: the long tail of cache-compression wins is bigger than the one big win, and the long tail of embedded-Rust targets is bigger than the ARM Cortex-M family.

Disclosure

Drafted with AI assistance. Primary source: FractalFir, crustc repository on GitHub (https://github.com/FractalFir/crustc), README verified by direct fetch on 2026-07-03 with curl --compressed. Source for: the 46-million-line C output figure; the cilly toolchain name; the "14th attempt" framing; the witness-program code blocks (TLS probe, sizeof(float) == sizeof(double) assertion); the triple JSON config (sdcc_z180-unknown-none, executable /usr/bin/sdcc, args -mz180 --std-c89 -c); the build timing (make -j20 937.98s user 123.77s system 1352% cpu 1:18.48 total); the resulting rustc --version output (rustc 1.98.0-nightly (c712ea946 2026-06-16)); the toolchain date (rustup install nightly-2026-06-16); the developer's environment (uname -a Linux spark-2773 6.17.0-1021-nvidia #21-Ubuntu SMP PREEMPT_DYNAMIC Wed May 27 19:14:05 UTC 2026 aarch64 aarch64 aarch64 GNU/Linux); the GCC version (GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0); the ABI caveat on ARM64 sret for sub-16-byte structs; the path-canonicalization bug in the build directory; the personal paragraph about the broken G and C keys, the job, the university, and the blender injury (paraphrased from the README; this blog's gloss, not a direct quote); and the "this is a demo/teaser" framing of the repository. The secondary source: Hacker News thread for item 48768464, "crustc: entirety of rustc, translated to C," submitted 2 July 2026 by Philpax, 321 points and 61 comments as of 03 July 2026 evening UTC+8 (count from HN Algolia API, both per-item and search-index endpoints). Source for: the Tiberium performance question, the taris2 Diverse Double-Compiling suggestion (paraphrased; the verbatim comment is "Have you tried Diverse Double-Compiling (DDC) to test if the official rust compiler has a backdoor? Use crustc to compile the rust source code, producing a new compiler. Then use this new compiler and the official rustc binary, both with deterministic flags, to compile the rust source code again. The two outputs should match bit for bit." per HN item 48768889, retrieved via curl --compressed against https://hn.algolia.com/api/v1/items/48768889 on 2026-07-03), the ronsor blender comment, the lioeters "14th attempt" observation, and the Cadwhisker link to the three-year-old landing page. These are HN comment framings, attributed as such. The character count and "the blender won" line in the body are paraphrases of the README's tone, not verbatim quotes. The "321 points and 61 comments" figure is the HN Algolia API count from a curl --compressed fetch on 2026-07-03; HN point counts are moving and the live thread at news.ycombinator.com/item?id=48768464 is the canonical figure.

Sources

  • FractalFir, crustc repository on GitHub — https://github.com/FractalFir/crustc. The repository's README is the primary source for: the 46-million-line C output figure; the cilly toolchain name and the "14th attempt" framing; the witness-program examples for _Thread_local and sizeof(float) == sizeof(double); the triple JSON config snippet (sdcc_z180-unknown-none with executable: /usr/bin/sdcc, base_args: [-mz180, --std-c89, -c], input_arg_template: ["{input}"]); the build timing on the developer's aarch64 host (make -j20 937.98s user 123.77s system 1352% cpu 1:18.48 total); the resulting --version output (rustc 1.98.0-nightly (c712ea946 2026-06-16)); the developer's workstation info (Linux spark-2773 6.17.0-1021-nvidia); the GCC 13.3.0 / Ubuntu 24.04 toolchain notes; the ABI caveat on ARM64 sret and the under-16-byte-struct exception; the path-canonicalization bug in the build directory ("I am confused too"); the network-transparency-over-TCP and Plan 9 demo transcript (Hello, world! with rust_begin_unwind symbol mangling); the personal paragraph on the broken G and C keys, the new job, university, and blender injury; the "I will not elaborate further" line on the blender; and the "this is a demo/teaser" framing. Verified via curl --compressed against https://raw.githubusercontent.com/FractalFir/crustc/main/README.md on 2026-07-03, returning HTTP 200 with a 4214-byte body that contains the title #crustc-rustc 1.98.0-nightly (c712ea946 2026-06-16), converted to 46 million lines of C. and the full ## What is this? and ## How was this done? and ## Why was this done? sections quoted in the body.
  • Hacker News thread, item 48768464 — https://news.ycombinator.com/item?id=48768464. Submitted 2 July 2026 by Philpax ("crustc: entirety of rustc, translated to C"), 321 points and 61 comments as of 03 July 2026 evening UTC+8 (count from HN Algolia API, both per-item and search-index endpoints agree on these figures). The body text is the canonical Algolia API response for this item, retrieved 2026-07-03 via curl --compressed against https://hn.algolia.com/api/v1/items/48768464. Source for: the Tiberium "how the performance looks" question, the taris2 Diverse Double-Compiling suggestion (per HN item 48768889, retrieved via curl --compressed against https://hn.algolia.com/api/v1/items/48768889 on 2026-07-03; the verbatim comment is paraphrased in the body — the DDC recipe is "use crustc to compile the rust source code, producing a new compiler, then use this new compiler and the official rustc binary, both with deterministic flags, to compile the rust source code again, and the two outputs should match bit for bit"), the ronsor "What a shame. I would've read an article about this" comment on the blender paragraph, the lioeters "Gotta respect the dedication to a niche interest" comment on the 14-attempt history, and the Cadwhisker link to the three-year-old landing page. These are HN comment framings, attributed as such. The 321/61 figures are the HN Algolia API count from a curl --compressed fetch on 2026-07-03; the live thread at the URL above is the canonical figure for any future reader.
  • Hacker News search API (secondary, for point-count verification) — https://hn.algolia.com/api/v1/search?query=crustc+rustc+c&hitsPerPage=5. Returned on 2026-07-03 with HTTP 200; the first hit matches item 48768464 with 321 points and 61 comments, agreeing with the per-item endpoint. The body uses these as the canonical figures; the live HN thread is the fallback for any future reader.

Thursday, June 18, 2026

Anubis Moved PoW to WebAssembly. The Compiler Broke It.

Xe Iaso's "I hate compilers" hit the front page of Hacker News on 18 June 2026 with 111 points, and the title undersells what is actually a reproducible-build horror story dressed up as a WASM-to-JavaScript engineering writeup. Anubis — the proof-of-work reverse proxy that this blog covered recently as the de facto answer to the LLM-scraper DDoS problem — is moving its challenge logic from SHA-256 to WebAssembly so administrators can swap in custom PoW schemes. The goal is clean: define the check logic once, run the same bytes on both client and server. The reality is that getting the same bytes out of clang twice in a row is the actual hard part.

The lesson generalizes well beyond Anubis — to anyone shipping compiled artifacts (WASM modules, native binaries, LLVM bitcode, kernel modules) from CI and expecting the bytes to be stable.

Angle 1: Why your WebAssembly binary has a different hash on every rebuild

The first demonstration in Xe's post is the reproducible-builds thesis in twenty lines of C++. The example defines __DATE__ and __TIME__ as compiler builtins that stamp the build timestamp into the output, then compiles the same hello.cpp twice in a row. The two outputs differ in the embedded timestamp. Identical source, different bytes — on every run, for a reason no one designing a "reproducible build" would have invented.

Compiler nondeterminism shows up in three places that the Anubis writeup hits in order: embedded timestamps via __DATE__ / __TIME__ (trivial); tooling the compiler shells out to, like Clang silently invoking wasm-opt from $PATH (surprising); and address-sensitive codegen, where pointer values leak into the order of try_table blocks in Clang's exception-handling path (genuinely hard). Xe observed the last one as a 29-byte drift between consecutive builds of the same wasm2js on the same machine with the same flags. Structurally meaningless, byte-for-byte meaningful.

@pertymcpert identified the mechanism in the HN comments: Clang iterating over a DenseMap (a hash-map with non-deterministic iteration order) on some code path when generating try_table blocks; the fix is to swap for a MapVector (preserves insertion order, with some runtime/memory cost). One-line fix in Clang. Until it ships, every WASM binary built from C++ with exception handling will drift on every build.

Angle 2: The tooling supply chain is the actual attack surface

The most operationally alarming finding is the chain clang → wasm-opt → binaryen → wasi-sdk → Clang's bundledwasm2js`. Every one has its own version, schedule, and vendoring story. Thewasm-optXe had on a DGX Spark ARM machine was 108. The version on his x86 workstation, from Homebrew, was 130. The version Clang reaches for depends on$PATH. When the installedwasm-optis too old to understand the WebAssembly Exceptions extension thatwasi-sdk` emits by default, the build fails silently — looks like a Clang bug, is a binaryen version mismatch.

The lesson: the compiler's "implicit dependencies" are not in your lockfile. Nix picks this up — @crvdgc pointed out in the comments that Nix sets the build time to epoch to make hash calculation stable — but most CI pipelines do not. Pinning clang alone is insufficient; pin every binary the compiler can shell out to.

For Anubis — where the WASM binary is the trust anchor for the entire proof-of-work challenge — the compiler's nondeterminism lands as a security boundary. Reproducible builds are the property that lets an independent party re-build your binary, compare hashes, and be confident they got what you shipped. Without it, the "is this WASM actually from the Anubis project?" question becomes unanswerable.

Angle 3: The fallback chain is more honest than most production stacks

The original WASM-based PoW challenge had one failure mode: a client with WebAssembly disabled (privacy settings, browser policy, an old embedded device, Tor Browser) cannot solve the challenge and gets locked out. Xe did not want to exclude those users, so:

  1. Primary: WASM check, runs on both client and server, fast.
  2. Fallback when WASM is disabled: wasm2js recompiles the same WASM module into JavaScript at build time. Slower, but it runs on any browser.
  3. Why both artifacts stay byte-equal: the WASM and the JS both encode the same source, so the PoW logic is identical. The browser picks one.

The original-recipe implementation uses wasm2js from the Linux distribution's package manager. That's where the reproducibility problem comes in: Debian's version is too old, Homebrew's produces different output, and the version Clang produces depends on $PATH. Xe's fix is to bundle a copy of wasm2js compiled to WASM with wasi-sdk, and ship it inside the Anubis repo. Single-architecture, single-toolchain, byte-stable (modulo the Clang bugs above).

A generic "WASM is the answer" stack would ship the WASM-only path and add a "supported browsers" list. Xe's stack is "if you can't run WASM, run our slower JS port, and we keep both artifacts under the same reproducibility guarantee." The fallback is part of the product, not a TODO.

Angle 4: This is the second anti-AI-bot arms escalation that depends on toolchain trust

The first escalation was the original Anubis PoW: a SHA-256 challenge that proves the client spent CPU. It works because SHA-256 is in WebCrypto on every browser and the CPU cost is honest. The second escalation moves the challenge itself into a WASM module, giving the server operator control over the PoW scheme — memory-hard, GPU-unfriendly, custom preimage format, all without coordinating with the Anubis core team.

The new attack surface is the WASM module itself. With SHA-256, the trust chain was Anubis project → npm package → your server → browser. With WASM, it is Anubis project → WASM binary built by someone → mirrored to a CDN → loaded by the browser. The honest defense is reproducible builds. Xe's whole post is an open admission that the reproducible-builds half of that defense is missing for the toolchain he is using, plus a working note on the patches he applied to make it so.

Angle 5: The HN thread shows the canonical mistakes

Three top comments identify the three common wrong responses to "this build is non-deterministic":

  • @charcircuit: byte-identical output is an arbitrary restriction, equivalent programs are equivalent regardless of the build hash, the right defense is signature verification. Cryptographically correct in the narrow sense. Wrong for Xe's use case: Anubis is community-run and the trust model is anyone can rebuild and verify, not trust the single signing key holder.
  • @dyauspitr: LLMs should be trained on and directly output binary. The "skip the compiler" position. The determinism problem goes away when the model is the compiler — except it does not, it just moves.
  • @ComputerGuru pushed back on the title as clickbait, noting that compilers literally made the project possible. The right read. Xe hates compilers the way a structural engineer hates gravity: gravity is a real force, and you design around it anyway.

All three replies are partially correct in isolation. None engages with the actual problem: "I need this WASM binary reproducible so downstream operators can verify it."

The original take: the compiler is the supply chain

The honest read of "I hate compilers" is that the modern compiled-artifact supply chain has the same trust properties as a software dependency graph, and most projects are not treating it that way. You pin npm versions. You audit container base images. You run cargo audit or npm audit. You do not, as a rule, audit your clang's implicit wasm-opt dependency.

The reproducible-builds community has been saying this for fifteen years. Debian's reproducible-builds project has been patching individual nondeterminism sources across the archive. Nix, Guix, and Bazel-with-remote-execution each take a swing at the hermetic-build problem. None of them is the default.

Xe's post is, in this reading, a public service announcement that the Anubis team is one of the few projects in the WASM ecosystem taking the question seriously. They ship their own vendored wasm2js, accept the 29-byte Clang-exception-handling drift as a known-unfixed upstream bug, and document the patch trail. That is not "I hate compilers." That is "I have read the source code of my compiler and I am not happy about what I found, but here is the patch."

What this means for you

If you ship a WASM module, native binary, or any compiled artifact that downstream parties verify, ask this week:

  1. Two consecutive builds on the same machine — same bytes? Run three times, sha256sum the outputs.
  2. Two different machines, both pinned — same bytes? Pin clang, pin wasm-opt, pin everything clang can shell out to. strace -f -e execve the build, read what it invokes.
  3. If a downstream operator runs your build today, do they get the same bytes you got last month? If the answer is no, your signing story is the only thing standing between "trust us" and "trust us, plus our key." Decide before the audit asks.

If you are using Anubis (or any tool that ships a WASM PoW check), ask your vendor whether the WASM module you load is reproducible from a clean checkout. If they cannot answer, the "is this WASM actually from the project?" question is one CDN compromise from being unanswerable.

What to do this week

Pick a compiled artifact you ship and run this three times — same source, fresh build each time, hash the output:

make clean && make my-wasm-module
sha256sum my-wasm-module
make clean && make my-wasm-module
sha256sum my-wasm-module
make clean && make my-wasm-module
sha256sum my-wasm-module

If the three hashes disagree, the artifact is non-reproducible. The usual culprits, in order of frequency: embedded timestamps (__DATE__, __TIME__, build epoch); source paths in debug info (-ffile-prefix-map helps); compiler-shelled-out-to tooling (strace your build); address-sensitive codegen (MapVector vs DenseMap, etc.).

For Nix users the fix is partially built in:

nix-build -A my-wasm-module
nix-build -A my-wasm-module  # second build, same hash?

If the two builds disagree and you are not on Nix, the path forward is either Nix (heavy lift, real fix) or a hand-pinned toolchain inside a container with the tool versions frozen in the Dockerfile (lighter lift, recurring maintenance). Xe chose the second path for Anubis. Most projects do not choose either, and ship non-reproducible binaries anyway.

Disclosure

Drafted with AI assistance. Primary source (Xe Iaso's "I hate compilers") and the HN thread (item 48581070) were both retrieved via direct HTTP fetches on 2026-06-18 around 13:30 UTC. All quoted comments are paraphrased, not blockquoted; the compiler-nondeterminism claims (__DATE__ / __TIME__, Clang's silent wasm-opt shell-out, DenseMap vs MapVector for try_table ordering, the 29-byte drift) are sourced from Xe's writeup, with the MapVector mechanism confirmed in the comment by @pertymcpert. The 111-point HN figure is from the Algolia API at the fetch timestamp (live-page counter was 113 at the same moment; the API value is the canonical figure for citation). Xe Iaso is the author of Anubis; weight that into any verification claims about the toolchain.

The compiler is the supply chain. You are not auditing it.

Sources

  • Xe Iaso, "I hate compilers" — the primary writeup, with the full reproducible-builds walkthrough (published 2026-06-18, 1665 words): https://xeiaso.net/notes/2026/anubis-wasm-vendor-binary/
  • HN discussion, item 48581070, "I hate compilers" (111 points per Algolia API as of 2026-06-18 13:30 UTC fetch; live-page counter was 113 at the same moment): https://news.ycombinator.com/item?id=48581070
  • Anubis project, the proof-of-work proxy whose WASM-port this post is about: https://github.com/TecharoHQ/anubis
  • Binaryen / wasm2js, the WebAssembly-to-JavaScript transpiler Xe is vendoring for the deterministic-builds fix: https://github.com/WebAssembly/binaryen
  • wasi-sdk, the WASI-flavored Clang toolchain Xe used to compile wasm2js to WASM: https://github.com/WebAssembly/wasi-sdk
  • Related on this blog: "An AI Agent Burned $6,531 on AWS to Scan a Hobby Network Nobody Asked It To" — covers Anubis as the standard answer to LLM-scraper DDoS: https://tutorialoflife.blogspot.com/2026/06/an-ai-agent-burned-6531-on-aws-to-scan.html
  • Related on this blog: "Linear Is Fast Because the Browser Is the Database" — different problem, same supply-chain-trust theme: https://tutorialoflife.blogspot.com/2026/06/linear-is-fast-because-browser-is.html