Skip to content

chore(bazel): replace hermetic_cc_toolchain (zig cc) with hermetic-llvm - #10991

Merged
basvandijk merged 25 commits into
masterfrom
bas/hermetic-llvm
Aug 4, 2026
Merged

chore(bazel): replace hermetic_cc_toolchain (zig cc) with hermetic-llvm#10991
basvandijk merged 25 commits into
masterfrom
bas/hermetic-llvm

Conversation

@basvandijk

@basvandijk basvandijk commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Replaces Uber's hermetic_cc_toolchain 4.3.0 (zig cc) with hermetic-llvm — BCR module llvm 0.8.16, a prebuilt clang/lld (LLVM 22.1.8) zero-sysroot cross toolchain — and removes every /tmp/zig-cache reference.

Why

hermetic_cc_toolchain required a zig cache (/tmp/zig-cache) which proved problematic and had to be patched. A newer version fixed the need for the patch but it failed because of another reason on RBE.

Additionally zig cc required four local patches and a pile of plumbing that all disappear with a plain clang/lld toolchain:

Zig workaround Why it's gone
hermetic_cc_toolchain_strip.patch (out-of-band host strip -S for determinism) + rust.strip_level_select(none) clang emits deterministic debug info; rules_rust's default opt = -Cstrip=debuginfo keeps symbols, sheds DWARF
hermetic_cc_toolchain_cache_dir.patch (zig cache race under RBE) + all /tmp/zig-cache mounts/mkdirs clang/lld has no compile cache; runtimes are ordinary sandboxed Bazel actions
hermetic_cc_toolchain_lto.patch (zig's lld fork warned on rustc's -Wl,-O1) upstream lld accepts -O1 silently
cc_rs.patch (rewrote LLVM triples into zig naming) standard triples are correct for clang

Toolchain wiring

  • hermetic-llvm's extension has no extra_target_settings, so registration goes through wrapper toolchain()s in //bazel gated on the existing //bazel:use_hermetic_cc. --hermetic_cc=false / --config=afl behave exactly as before (verified with --toolchain_resolution_debug: wrappers rejected → falls through to local_config_cc).
  • macOS: hermetic for wasm32 (canister) targets only. The zig none_wasm toolchain had no exec constraints, so canister builds on macs relied on it; the macos_*→none_wasm32 wrappers keep that working (wasm needs no Apple SDK). Native macOS targets stay on the system Xcode toolchain: hermetic-llvm's macOS support (trimmed SDK sysroot, resource-dir handling) is not yet mature enough for the full darwin build — the first CI round failed in tikv-jemalloc-sys's configure-based build script (math.h not found).
  • glibc floor stays pinned at gnu.2.31 (zig parity) via //:linux-glibc-2.31 (a @platforms//host-parented platform) + build:linux --platforms/--host_platform. The 2.28 default was tried first per plan, but gettid (glibc 2.30) users in rs/ic_os hit the documented contingency.
  • Host-library -L leak class (found by CI): cargo build scripts probing host libs via pkg-config inject -L/usr/lib/x86_64-linux-gnu ahead of the hermetic search paths, making -lc resolve to the host glibc while crt objects come from the hermetic one (undefined __libc_csu_init/__libc_csu_fini). zig was immune (it resolved libc internally). Fixed per crate: pcre2-sys and libusb1-sys now build their bundled C sources (more hermetic than before); devicemapper-sys / libcryptsetup-rs-sys / libsystemd-sys link the host library by explicit path via cc_import host-import repos (the existing @libsystemd pattern) with a pkg-config wrapper (bazel/pkg_config_no_link_metadata.sh) that strips -L/-l from probe output while keeping version checks and bindgen include paths; virt-sys / libcryptsetup-rs are probe-only and just get the wrapper.

Missing-unwinder link class (found by CI, round 2)

hermetic-llvm links with --unwindlib=none and stubs out libgcc_s/libunwind with empty archives; the only provider of the _Unwind_* symbols rust std needs is the toolchain's static runtime (cc_toolchain.static_runtime_lib = LLVM's libc++/libc++abi/libunwind archives). rules_rust drops that runtime in two places — zig was immune because zig cc always injected its own unwinder. Both fixed by a patch on the rules_rust fork (bazel/rules_rust_static_runtime.patch; TODO: upstream to dfinity/rules_rust and drop on the next fork bump):

  • Every rust_doc_test failed to link (undefined symbol: _Unwind_Resume) — ~34 of the 50 "Bazel Test All" failures and the single arm64-linux failure. rustdoc test links pass include_link_flags = False, which also skipped the static runtime's -lstatic flags; the doc-test writer additionally didn't rewrite the runtime's -Lnative search paths (which live under the toolchain's configuration root) into runfiles-relative form for the test-time link.
  • rust_shared_library silently linked no unwinder — the actual cause of all 17 failing system tests (canister_http_*, firewall_*, api_bn_decentralization_*). rules_rust links the dynamic C++ runtime into dylib/cdylib targets, and hermetic-llvm's dynamic runtime consists of versioned files (libunwind.so.1) that -lunwind never matches, so lld silently fell back to the empty stub archive (shared-library links don't error on undefined symbols). At runtime: dlopen of libnss_icos.so failed with undefined symbol: _Unwind_GetIP → GuestOS couldn't resolve the hostos hostname (provided by that NSS plugin) → nft -f rejected the orchestrator-generated ruleset (it whitelists hostos by name: ip6 saddr { hostos }) → the ip6 filter table never appeared, which is exactly what those tests wait for. Diagnosed by SSHing into a keepalive testnet node: reload_nftables.service in a restart loop with Error: Could not resolve hostname. Fix: prefer the static runtime for dylib/cdylib links whenever the toolchain provides one (local/Xcode toolchains provide none and keep their dynamic-runtime behavior).

Fallout fixed along the way

  • rs/canister_sandbox's backtrace.c includes <libunwind.h> (zig bundled it): new //bazel:libunwind_include (via bazel/cc_headers_dir.bzl) exposes @llvm-project//libunwind headers to the cc-rs build script. LLVM libunwind is statically linked by the toolchain runtime libs — verified no new runtime .so dependency.
  • aws-lc-sys 0.42.0's memcmp compiler probe ignores CFLAGS and can't compile under a zero-sysroot toolchain. Bumped aws-lc-rs 1.17.3 / aws-lc-sys 0.43.0, where the probe is (correctly) skipped for clang — the miscompile it probes for is GCC-specific.
  • rustc 1.97's linker_messages lint surfaced benign clang driver chatter on every link; the lint is allowed in bazelrc (linker errors still fail the build).
  • libusb-1.0-0-dev dropped from the dev container: the only consumer (vsock_lib's rusb, for HSM attach) now builds the bundled libusb in both build systems — the vendored feature is enabled on the rusb crate.spec (Bazel) and in vsock_lib's Cargo.toml (cargo). Verified: vsock_host links libusb statically with no dynamic libusb NEEDED; cargo check -p vsock_lib passes.
  • Fuzzing simplification unlocked by clang: DEFAULT_SANITIZERS dropped the zig-era -Zexternal-clangrt + hardcoded -Clink-arg=…librustc-stable_rt.asan.a pair — rustc's default mechanism links the same bundled ASan runtime itself under clang/lld, without a path that baked in the compilation mode, the rules_rust canonical repo name, the toolchain channel and the arch. Verified: libfuzzer target links exactly one ASan runtime (nm: one defined __asan_init) and runs 100 iterations; AFL target builds green (unaffected by design).
  • rules_cc 0.2.18 → 0.2.22, rules_go 0.61.1 → 0.62.0 (matching MVS resolution with llvm@0.8.16 in the graph).

🤖 Generated with Claude Code

Swap the hermetic C/C++ toolchain from Uber's hermetic_cc_toolchain 4.3.0
(zig cc, consumed via an archive_override carrying three local patches) to
the BCR module `llvm` 0.8.16 (https://github.com/hermeticbuild/hermetic-llvm),
a prebuilt clang/lld (LLVM 22.1.8) zero-sysroot cross toolchain.

Toolchain wiring:
* hermetic-llvm's extension has no extra_target_settings support, so the
  toolchains are registered through wrapper toolchain()s in //bazel that are
  additionally gated on //bazel:use_hermetic_cc. --hermetic_cc=false (and
  thus --config=afl's host-toolchain fallback) keeps working unchanged.
* macOS goes hermetic too: hermetic-llvm downloads the macOS SDK from
  Apple's CDN, removing the old blocker for hermetic Darwin toolchains.
  This also keeps wasm/canister builds working on macs, which previously
  relied on zig's exec-unconstrained none_wasm toolchain.
* The default glibc floor moves from gnu.2.31 (zig) to hermetic-llvm's
  default gnu.2.28.

Zig-specific debt removed:
* hermetic_cc_toolchain_strip.patch: clang emits deterministic debug info;
  dropping rust.strip_level_select restores the rules_rust default
  (opt = -Cstrip=debuginfo), which keeps symbols and sheds DWARF without
  the out-of-band host `strip -S`.
* hermetic_cc_toolchain_cache_dir.patch: clang/lld has no compile cache,
  so the zig cache race workaround and ALL /tmp/zig-cache plumbing
  (bazelrc repo_env/sandbox mounts, dev container mounts, CI mkdirs,
  bazel_clean.sh) are deleted with no replacement.
* hermetic_cc_toolchain_lto.patch: upstream lld does not warn on rustc's
  -Wl,-O1 (that was zig's lld fork).
* cc_rs.patch: cc-rs's standard LLVM triples are correct for clang; the
  zig triple rewriting is gone (crate annotation removed + repin).

Fallout fixed along the way:
* rs/canister_sandbox's backtrace.c includes <libunwind.h>, which zig
  bundled but zero-sysroot clang does not: the new //bazel:libunwind_include
  (via bazel/cc_headers_dir.bzl) exposes @llvm-project//libunwind headers
  to the cc-rs build script; LLVM libunwind is statically linked by the
  toolchain's runtime libs, so -lunwind resolves with no new runtime dep.
* aws-lc-sys 0.42.0's memcmp compiler probe ignores CFLAGS and cannot
  compile under a zero-sysroot toolchain; aws-lc-rs 1.17.3 / aws-lc-sys
  0.43.0 skip the probe for clang (the bug it probes for is GCC-specific),
  so bump instead of patching.
* rustc 1.97's linker_messages lint surfaces benign clang driver chatter
  (`--unwindlib=none` unused); allow the lint. A driver-level -Wno- flag
  is not an option since wasm targets link through rust-lld directly.
* rules_cc 0.2.18 -> 0.2.22 and rules_go 0.61.1 -> 0.62.0, matching what
  MVS resolves with llvm@0.8.16 in the graph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the Bazel C/C++ hermetic toolchain from Uber’s hermetic_cc_toolchain (zig cc) to hermetic-llvm (llvm BCR module 0.8.16), updates supporting Bazel/Rust wiring, and removes all zig-cache plumbing.

Changes:

  • Replace zig-based hermetic toolchain registration with hermetic-llvm toolchain extension + wrapper toolchain() targets gated by //bazel:use_hermetic_cc.
  • Remove /tmp/zig-cache mounts/setup and delete now-unneeded zig-specific patches/config.
  • Add a small Bazel rule to expose libunwind headers to cc-rs build scripts and bump aws-lc-* to versions that work under the new toolchain assumptions.

Reviewed changes

Copilot reviewed 21 out of 25 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
third_party/BUILD.libvirt.bazel Updates rationale/comments for --allow-shlib-undefined under the new hermetic toolchain context.
third_party/BUILD.libsystemd.bazel Updates rationale/comments for --allow-shlib-undefined under the new hermetic toolchain context.
third_party/BUILD.e2fsprogs.bazel Simplifies compatibility comment (tools are Linux-only in practice).
rs/canister_sandbox/BUILD.bazel Adds CFLAGS include path + data dep to supply libunwind.h for cc-rs compilation.
MODULE.bazel Swaps module deps to llvm@0.8.16, bumps rules_cc/rules_go, and reworks toolchain extension setup/registration.
ci/container/init.sh Removes creation/permissioning of /tmp/zig-cache.
ci/container/Dockerfile Updates linker-related comment to reflect clang/lld toolchain; keeps ld.lld symlink.
ci/container/container-run.sh Removes zig-cache bind mount and directory creation.
Cargo.lock Bumps aws-lc-rs/aws-lc-sys versions/checksums.
Cargo.Bazel.toml.lock Mirrors the aws-lc-* version/checksum bumps for Bazel cargo integration.
Cargo.Bazel.json.lock Updates crate metadata/links for aws-lc-* and removes cc patching entries.
bazel/rust.MODULE.bazel Removes zig-era strip override and cc crate patch; updates sanitizer comment.
bazel/hermetic_cc_toolchain_strip.patch Deletes zig-toolchain determinism workaround patch.
bazel/hermetic_cc_toolchain_lto.patch Deletes zig-lld warning suppression patch.
bazel/hermetic_cc_toolchain_cache_dir.patch Deletes zig-cache race workaround patch.
bazel/fuzz_testing.bzl Updates sanitizer comment to reflect clang toolchain context.
bazel/conf/.bazelrc.build Removes zig-cache-related sandbox/cache config; adds rustc lint allow + macOS archiver flag.
bazel/cc_rs.patch Deletes zig target-triple rewrite patch for cc-rs.
bazel/cc_headers_dir.bzl Introduces rule to materialize headers into a directory artifact for build scripts.
bazel/BUILD.bazel Adds libunwind_include target and wrapper hermetic-llvm toolchains gated on :use_hermetic_cc.
bazel/bazel_clean.sh Removes cleanup of /tmp/zig-cache.
.github/workflows/ci-main.yml Removes CI step creating /tmp/zig-cache.
.github/actions/bazel/bin/bazel Removes zig-cache setup from the GitHub Action wrapper.
.devcontainer/devcontainer.json Removes zig-cache directory creation and bind mount.
.claude/skills/run-in-dev-container/SKILL.md Updates documentation to remove zig-cache mention.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ci/container/Dockerfile Outdated
…a8e56c181e7b2ff9fa5b4d2a4f7ec

ic-build: sha256:2b18bc15d1c2f503e125cf52a2bfda9f2bcd2f85d9bb34a600834fc69e18b76f

ic-dev:   sha256:162a908c48e6bdf0611077cc252ebe53ccb76f8e37c724e82e3cf847d38c4ea0
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Run URL: https://github.com/dfinity/ic/actions/runs/30750926226

New container images with tag: 827366520f53ae0ab466ed38c5103df6d6726f45aa5789be235bfb5c06797ccb
ic-build: sha256:2a4aed7210bdf3522219410bc14c0ca8161206c02c3cc9f24409f2cb5233f9b5
ic-dev: sha256:484507d3a8cbd75d81f4cc2e990efdde7a780fefacd6d47d0362032f1bfd7968

…cOS wasm-only

Address the CI failures of the hermetic-llvm migration and the Copilot
review comment.

Host -L leaks (Bazel Test All / Build IC / repro-check failures):
cargo build scripts that probe host libraries via pkg-config emit the host
library dir (-L/usr/lib/x86_64-linux-gnu) ahead of the hermetic toolchain's
library search paths. lld then resolves `-lc` against the HOST glibc while
the crt objects come from the hermetic glibc — an incompatible mix
(undefined __libc_csu_init/__libc_csu_fini, host GLIBC_ symbol versions).
zig cc was immune because it resolved libc internally, ignoring -L order.
Fixes, per crate:
* pcre2-sys: build the bundled pcre2 (PCRE2_SYS_STATIC=1) — strictly more
  hermetic than linking the host pcre2.
* libusb1-sys: build the bundled libusb ("vendored" feature); disable the
  libudev probe so it uses the netlink hotplug backend.
* devicemapper-sys, libcryptsetup-rs-sys, libsystemd-sys: genuinely host
  libraries — link them by explicit path via cc_import host-import repos
  (the existing @libsystemd pattern; new @DevMapper and @cryptsetup repos),
  and route the pkg-config probe through a wrapper
  (bazel/pkg_config_no_link_metadata.sh) that strips -L/-l from its output
  while keeping version checks and -I include paths for bindgen.
* libcryptsetup-rs, virt-sys: probe-only build scripts; same wrapper.

glibc floor (planned contingency): with the host dirs gone, links surfaced
`undefined symbol: gettid` — gettid is glibc 2.30, hermetic-llvm defaults
to a 2.28 sysroot. Pin gnu.2.31 (parity with the zig toolchains) via
//:linux-glibc-2.31 (host-parented platform) and build:linux
--platforms/--host_platform. Verified: hermetic toolchain resolves on the
pinned platform, --hermetic_cc=false still falls through to the local
toolchain, and produced binaries stay at the 2.31 symbol floor.

macOS (Bazel Test arm64-darwin failure): hermetic-llvm's macOS support
(trimmed Apple SDK sysroot, resource-dir handling) is not yet mature enough
for the full darwin build — configure-based cargo build scripts
(tikv-jemalloc-sys) fail to find SDK headers. Scope the hermetic macOS
toolchains down to wasm32 (canister) targets only, which need no SDK; native
macOS targets stay on the system Xcode toolchain exactly as before this
migration. The arm64-linux job was fail-fast collateral of the darwin
failure.

Also reword the ci/container/Dockerfile lld comment to cover the
--hermetic_cc=false fallback (Copilot review).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 47 changed files in this pull request and generated no new comments.

Suppressed comments (2)

bazel/BUILD.bazel:89

  • The wrapper toolchains construct OS constraint labels from the string "macos" (via "@platforms//os:" + exec_os/target_os). Elsewhere in the repo, macOS constraints are consistently referenced as @platforms//os:osx (e.g. //:macos-intel in BUILD.bazel), so these toolchains may not match on Darwin and wasm-on-mac toolchain fallback could break.
        exec_compatible_with = [
            "@platforms//os:" + exec_os,
            "@platforms//cpu:" + exec_cpu,
        ],
        target_compatible_with = [
            "@platforms//os:" + target_os,
            "@platforms//cpu:" + target_cpu,
        ],

MODULE.bazel:1387

  • The comment "Use libvirt from the host environment" now sits above the cryptsetup/devmapper repositories, which is misleading and makes it harder to understand why these repos are declared. Consider adding a dedicated comment for cryptsetup/devmapper and keeping the libvirt comment directly above the libvirt repo.
# Use libvirt from the host environment
# Used for managing virtual machines programmatically.
new_local_repository(
    name = "cryptsetup",
    build_file = "@//third_party:BUILD.cryptsetup.bazel",
    path = "/usr",
)

…0097cae40fa542b56e2c891b30a66

ic-build: sha256:dd6ecee0bd582a060c658698602df96215f70885d88e702f47d35adf1e6938ab

ic-dev:   sha256:6368c561a3e2bf9a6765ff7978f4ea5f742e8ee27c4c957cbaad2c5c078661a7
…s it

hermetic-llvm links with --unwindlib=none and stubs out libgcc_s and
libunwind with empty archives: the only provider of the _Unwind_*
symbols rust std needs is the toolchain's static runtime
(cc_toolchain.static_runtime_lib = LLVM's libc++/libc++abi/libunwind
archives). rules_rust drops that runtime in two places — zig cc was
immune because it always injected its own unwinder. Both are fixed by
a patch on the rules_rust fork (bazel/rules_rust_static_runtime.patch,
to be upstreamed to dfinity/rules_rust and dropped on the next bump):

1. Every rust_doc_test failed to link with "undefined symbol:
   _Unwind_Resume" (~34 of the 50 Bazel Test All failures, and the
   single arm64-linux failure): rustdoc test links pass
   include_link_flags = False, which also skipped the static runtime's
   -lstatic flags, and the doc-test writer didn't rewrite the runtime's
   -Lnative search paths (which live under the toolchain's
   configuration root) into runfiles-relative form.

2. rust_shared_library (cdylib) targets silently linked no unwinder —
   the cause of all 17 failing system tests (canister_http_*,
   firewall_*, api_bn_decentralization_*): rules_rust links the
   *dynamic* C++ runtime into dylib/cdylib targets, and hermetic-llvm's
   dynamic runtime consists of versioned files (libunwind.so.1) that
   -lunwind never matches, so lld silently fell back to the empty stub
   archive (shared-library links don't error on undefined symbols). At
   runtime, dlopen of libnss_icos.so failed with "undefined symbol:
   _Unwind_GetIP", GuestOS could not resolve the "hostos" hostname
   (provided by that NSS plugin), nft -f rejected the
   orchestrator-generated firewall ruleset (it whitelists "hostos" by
   name in "ip6 saddr { hostos }"), and the ip6 filter table never
   appeared — which is exactly what those tests wait for. Fixed by
   preferring the static runtime for dylib/cdylib links whenever the
   toolchain provides one; local and Xcode toolchains provide none and
   keep their current dynamic-runtime behavior.

Verified locally: all 58 rust_doc_test targets pass; libnss_icos.so
dlopens cleanly with all 7 NSS symbols exported and no undefined
_Unwind_*; canister_http_correctness_test, firewall_priority_test and
api_bn_decentralization_test pass end-to-end on Farm; the ledger
canister and pocket-ic builds are pure action-cache hits (the patch
changes no non-cdylib, non-doc-test link); bazel build //... --nobuild
and buildifier are clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 44 out of 48 changed files in this pull request and generated no new comments.

Suppressed comments (2)

bazel/cc_headers_dir.bzl:24

  • The run_shell script doesn't enable set -e/pipefail, so a failed cp/mkdir could be silently ignored and the action would still succeed with an incomplete header directory. Adding set -euo pipefail makes failures propagate and avoids hard-to-debug missing-header errors later in the build.
        command = """
out="$1"; strip="$2"; shift 2
for f in "$@"; do

MODULE.bazel:1382

  • This comment block now sits above the cryptsetup/devmapper new_local_repository() declarations, so it no longer accurately describes the code that follows (it reads as if cryptsetup is libvirt). Updating the comment to cover all the host-provided libraries here will avoid confusion when editing this section later.
# Use libvirt from the host environment
# Used for managing virtual machines programmatically.

With -Zsanitizer=address, rustc automatically links its bundled ASan
runtime (librustc-stable_rt.asan.a). The zig toolchain mishandled that
mechanism, so DEFAULT_SANITIZERS suppressed it with -Zexternal-clangrt
and manually re-added the same archive via a -Clink-arg with a
hardcoded path — one that baked in the compilation mode (k8-opt), the
bzlmod canonical repo name of rules_rust, the toolchain channel and the
target arch, breaking silently whenever any of those changed. With a
plain clang/lld driver the default mechanism just works, so both flags
go away and only -Zsanitizer=address remains.

Verified: a libfuzzer target builds with exactly one ASan runtime
(nm: one defined __asan_init, none undefined) and runs 100 iterations
with live coverage feedback; an AFL target builds green (that path
never used DEFAULT_SANITIZERS at link — afl-clang-lto brings the host
ASan runtime); bazel build //... --nobuild and buildifier are clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 49 changed files in this pull request and generated no new comments.

Suppressed comments (1)

bazel/cc_headers_dir.bzl:30

  • The run_shell script doesn't enable set -e, so a failed mkdir/cp inside the loop could be ignored and the action might still exit 0 (depending on the last command executed). This could silently produce an incomplete headers directory and lead to confusing downstream compile failures. Consider enabling set -eu at the top of the script so the action fails immediately on errors.
        command = """
out="$1"; strip="$2"; shift 2
for f in "$@"; do
    rel="${f#*"$strip"}"
    [ "$rel" = "$f" ] && continue
    mkdir -p "$out/$(dirname "$rel")"
    cp -L "$f" "$out/$rel"
done
""",

basvandijk and others added 2 commits August 2, 2026 13:36
The if/else was buffering pkg-config's output only to suppress it on
failure and re-raise the status by hand. `pipefail` already propagates a
failing pkg-config through the pipeline, and pkg-config writes nothing to
stdout when it fails, so the buffering bought nothing.

Dropping the command substitution also stops normalizing the trailing
newline, which the `printf '%s\n'` was re-adding even for modes that emit
zero bytes. The pkg_config crate only ever runs `--libs --cflags` and
`--modversion`, all of which emit a trailing newline, so the output is
byte-identical for every invocation this wrapper actually sees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The only libusb consumer is vsock_lib's rusb dependency (the HSM-attach
flow). The Bazel build already compiles the bundled libusb (vendored,
netlink hotplug backend) since the hermetic-llvm migration, and cargo
builds now do the same: the `vendored` feature is enabled on rusb in
both vsock_lib's Cargo.toml (for cargo) and the rusb crate.spec (for
Bazel; feature resolution then puts it on libusb1-sys, replacing the
crate_features annotation). With neither build probing the system
library, the container package can go.

Verified: the lockfile resolves libusb1-sys with the vendored feature;
vsock_host links libusb statically with no dynamic libusb NEEDED;
`cargo check --all-targets --all-features -p vsock_lib` passes
(libusb1-sys's build.rs also falls back to the bundled source on its
own when pkg-config finds nothing, so cargo builds keep working even
without the feature); bazel build //... --nobuild and buildifier are
clean. Cargo.lock is unchanged (features don't affect it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@basvandijk

Copy link
Copy Markdown
Collaborator Author

Would bazel/rules_rust_static_runtime.patch still be needed with rules_rs?

hermetic-llvm's README recommends using rules_rs for Rust + hermetic-llvm interop, so I looked into whether switching to it would obsolete this PR's rules_rust patch.

TL;DR: yes — with rules_rs the patch would no longer be required. rules_rs isn't an independent ruleset: it's a layer on top of a pinned hermeticbuild/rules_rust fork (currently hermeticbuild/rules_rust@0105631), and that fork already fixes both problem classes the patch addresses — just by different means. One of the two fixes would move from "automatic" to a per-target attribute we'd need to set.

Recap: why the patch exists

hermetic-llvm links with --unwindlib=none and stubs libgcc_s/libunwind as empty archives, so the only provider of the _Unwind_* symbols rust std needs is the cc_toolchain's static_runtime_lib (LLVM's libc++/libc++abi/libunwind archives). Upstream rules_rust 0.71.3 drops that runtime in two places:

  1. Doc tests pass include_link_flags = False, skipping the static runtime's -lstatic flags (every rust_doc_test fails with undefined symbol: _Unwind_Resume), and the doc-test script writer doesn't rewrite the runtime's -Lnative paths into runfiles-relative form.
  2. dylib/cdylib links use the dynamic runtime via -ldylib=unwind, which never matches hermetic-llvm's versioned libunwind.so.1 — lld silently links no unwinder and the breakage only surfaces at dlopen time (the libnss_icos.so / GuestOS failure).

How the hermeticbuild fork fixes the same things

  • Doc tests: their rustdoc.bzl passes include_link_flags = is_test instead of hardcoded False, so the static runtime's link flags flow through construct_arguments naturally. And rust_doc_test was redesigned to run rustdoc as a build action (a RustdocTest action capturing stdout/stderr/exit code) rather than a script that relinks at test runtime — which eliminates the path-stripping problem (this patch's third hunk) by construction. They even carry a regression test, rustdoc_musl_unwind_link_flags_test, asserting the libunwind link arg is present on the doc-test action.
  • dylib/cdylib: the fork still defaults to the dynamic runtime, but on non-Windows it links it by full path via -Clink-arg=<path> instead of -ldylib=<name> (their comment: --as-needed could drop libunwind before libstd references it). A full path matches libunwind.so.1 fine, so the "silently missing unwinder" link-time failure mode is gone. It also adds those libs to the rpath.

The one caveat for us

The fork's dylib fix links the runtime dynamically — the resulting .so gets a NEEDED libunwind.so.1 entry, and the rpath points into the Bazel execroot. For shared libraries deployed outside Bazel (exactly libnss_icos.so on GuestOS, where hermetic-llvm's libunwind.so.1 doesn't exist on the image), that would still break at runtime. The fork's answer is an opt-in attribute on rust_shared_library:

cc_runtime_linkage = "static"  # embed libc++/libc++abi/libunwind → self-contained .so

So instead of this patch's "prefer static whenever the toolchain has one" global default, we'd set this on the handful of deployed rust_shared_library targets. Arguably cleaner (explicit, no fork patch), but it's a behavioral difference to remember — forgetting it on a new deployed cdylib reproduces the failure, though now at dlopen with a missing library (loud, obvious) rather than a missing symbol.

Bigger picture

Dropping the patch wouldn't be the hard part of that switch. Moving to rules_rs means replacing the archive_override on dfinity/rules_rust in bazel/rust.MODULE.bazel with hermeticbuild's fork (worth auditing what else the dfinity fork carries), and — the big one — replacing crate_universe with rules_rs's crates.from_cargo, which consumes Cargo.lock directly and would obsolete the Cargo.Bazel.*.lock files and the bazel-pin.sh workflow entirely. rules_rs's rules_rust extension does accept extra patches on the fork, so there's an escape hatch if something else comes up.

Short of that migration, the existing TODO (upstream this patch to the dfinity/rules_rust fork and drop it on the next fork bump) remains the low-effort path.

🤖 Generated with Claude Code

Comment thread bazel/rules_rust_static_runtime.patch
@basvandijk
basvandijk enabled auto-merge August 4, 2026 09:46
@basvandijk
basvandijk added this pull request to the merge queue Aug 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Aug 4, 2026
@basvandijk
basvandijk enabled auto-merge August 4, 2026 11:13
@basvandijk
basvandijk added this pull request to the merge queue Aug 4, 2026
Merged via the queue into master with commit 9e29205 Aug 4, 2026
54 checks passed
@basvandijk
basvandijk deleted the bas/hermetic-llvm branch August 4, 2026 12:22
Rachit2323 pushed a commit to Rachit2323/ic that referenced this pull request Aug 10, 2026
## Problem

`repro-check --guestos` fails for every commit since 9e29205 (the zig
cc → hermetic-llvm switch, dfinity#10991): the locally built GuestOS
`update-img.tar.zst` hashes differently from the CI/CDN artifact (e.g.
local `fc824312…` vs CDN `2f31b0e5…` at 9e29205), while being perfectly
stable across rebuilds on the same machine.

## Root cause

9e29205 set `PCRE2_SYS_STATIC=1` on `pcre2-sys` (to fix the
host-library `-L` leak class under the hermetic toolchain), which makes
the crate's build script compile the bundled PCRE2 C sources. That
`build.rs` enumerates the sources with an **unsorted
`std::fs::read_dir`**, so the member order of the produced `libpcre2.a`
is the build machine's filesystem iteration order — stable per machine,
but different across machines (ext4 iterates in name-hash order with a
per-filesystem seed chosen at mkfs time).

The order leaks into `open_rootfs` (the only pcre2 consumer shipped in
an IC-OS image, via `partition_tools`): lld records one `STT_FILE`
symbol per loaded archive member, in load order. All pcre2 *code* is
GC'd (`--gc-sections` — nothing in `open_rootfs` calls it), so the
binaries differ **only** in the order of their `pcre2_*.c` `STT_FILE`
symtab entries and the corresponding `.strtab` bytes. Diffing the local
vs CDN images file-by-file: `/opt/ic/bin/open_rootfs` is the **only**
differing file in the entire rootfs (boot partition differences are the
cascade: the initramfs embeds `open_rootfs`, `boot_args` carries the
root's verity hash, and `launch-measurements.json` follows).

Why the "Build Determinism" job didn't catch it: (a) on 9e29205 (and its
parent) it was **skipped** — `needs: [build-ic, bazel-test-all]` with no
`if:`, and Bazel Test All's *test* step failed after the artifacts had
already been uploaded; (b) even when it runs (verified green on later
master commits with the defect present), it compares two builds within
the uniformly-provisioned dind fleet, which agrees with itself on
readdir order — only a build outside the fleet exposes the difference.

## Fix

Patch the crate's `build.rs` to collect and sort the sources before
handing them to `cc::Build` (`bazel/pcre2-sys.patch`, wired via
`crate.annotation` like the existing `rustix`/`askama` determinism
patches, lockfile repinned via `./bin/bazel-pin.sh`). Same file set,
deterministic order → the archive is byte-identical regardless of the
build machine.

Upstream PR: BurntSushi/rust-pcre2#58

## Verification

- `patch -p1` applies cleanly against pristine pcre2-sys 0.2.8; `bazel
build //... --nobuild` and `bazel run //:buildifier` pass.
- `bazel build --config=local //rs/ic_os/open_rootfs:open_rootfs` in the
dev container: the rebuilt `libpcre2.a` member order is sorted (`ar t |
sort -c`), i.e. no longer filesystem-dependent (before the patch it
matched the machine's raw readdir order 1:1).
- Definitive confirmation will be the first `repro-check --guestos`
against a CI build that includes this commit.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants