#!/usr/bin/env bash # wasmcc-glibc: the glibc analogue of wasmcc (HWJS-B-2). A full cross C-compiler # driver that compiles/links bash, coreutils, /init and conftests against the # wasm32 GLIBC port (brintos/glibc branch wasm32) on the brintium coro arm. # # Differs from the musl wasmcc only in the libc it targets: # - headers : the durable glibc sysroot (make install-headers; §B-2 rig) # - target : wasm32-hwjs-linux-gnu on COMPILE (glibc headers need __linux__) # - startfile: crt1_glibc_hwjs.bc (the glibc-aware coro _start) # - libc : the glibc coro BITCODE pool (WASMLD_HWJS_LIBC_BC) + the native # .S objects archive (libglibc_native.a) — NOT musl-bc / -lc # - plugin : the submodule-derived HwjsCoroutinize.so (provenance-checked # against this checkout's HEAD; see below) # # COMPILE emits LLVM bitcode (the HwjsCoroutinize pass runs once over the merged # module in wasmld). LINK injects the glibc coro startfiles + drives wasmld with # the glibc pool. Assembly (.s/.S) has no IR form -> native compile. NEVER edits # bash/coreutils/glibc source — it only wires the toolchain. set -euo pipefail HWJS_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # shellcheck source=libexec/find-llvm.sh source "$HWJS_HOME/libexec/find-llvm.sh" # shellcheck source=libexec/link-direct.sh source "$HWJS_HOME/libexec/link-direct.sh" # The glibc sysroot, bitcode pool and startfiles all live under one tree, and # that tree is the sysroot directory of this checkout. The default used to # name a shared directory inside a distribution of the repository this # toolchain was a submodule of; it names nothing here. bin/brintcc resolves # and exports the pointer for every real invocation, so what changes is what a # direct call gets. No machine-specific absolute path. TOOLS_DIR="${HWJS_TOOLS_DIR:-${BRINTIUM_SYSROOT:-$HWJS_HOME/sysroot}}" HWJS_LLVM_BIN="$(hwjs_llvm_bin "$HWJS_HOME/libexec")" \ || { echo "wasmcc-glibc: FATAL: pinned LLVM not found (set WASMLD_HWJS_LLVM_BIN, or build it per toolchain/llvm-from-source.md)." >&2; exit 1; } CLANG="$HWJS_LLVM_BIN/clang" WASMLD="${HWJS_WASMLD:-$HWJS_HOME/libexec/brintld}" # ---- durable glibc libc (B-2 rig; override via env) ------------------------ GLIBC_BUILD="${GLIBC_BUILD:-$TOOLS_DIR/build/glibc-2.43-build}" GLIBC_SYSINC="${GLIBC_SYSINC:-$GLIBC_BUILD/sysroot/usr/include}" # Linux UAPI kernel headers (linux/*.h, asm/*.h) — glibc's headers (bits/ # local_lim.h -> linux/limits.h, etc.) include them; the same dir glibc # configure used as --with-headers. This arm puts them on EVERY command line # with -isystem, so an absent pointer is a refusal here. # # THERE IS NO DEFAULT, and there was one for years: a path into one particular # distribution's build tree, in a repository this toolchain no longer sits in. # It had already stopped being where the headers were installed before that, # and nothing failed, because clang ignores an -isystem naming a directory that # is not there — so the skew showed up as a missing linux/*.h in a glibc # compile with no hint that the wrong tree had been named. A guessed default is # also a guess at WHICH kernel, and a userland compiled against one kernel's # ABI for a guest that runs on another fails at a system call, not at a build # step. bin/brintcc refuses before it gets here; this covers a direct call. GLIBC_UAPI_INC="${GLIBC_UAPI_INC:-${BRINTIUM_KERNEL_HEADERS:-}}" [ -n "$GLIBC_UAPI_INC" ] || { echo "wasmcc-glibc: FATAL: neither BRINTIUM_KERNEL_HEADERS nor GLIBC_UAPI_INC is set, and this arm compiles against the kernel UAPI headers (linux/*.h, asm/*.h) on every line. They are installed by the kernel build, not by this toolchain, so there is nothing to fall back to. Set BRINTIUM_KERNEL_HEADERS to the installed UAPI include directory — the one holding linux/ and asm/." >&2 exit 1 } [ -d "$GLIBC_UAPI_INC" ] || { echo "wasmcc-glibc: FATAL: the kernel UAPI header directory '$GLIBC_UAPI_INC' does not exist. It must be the kernel build's installed UAPI include directory (the one holding linux/ and asm/), not a kernel source tree and not the install prefix above it." >&2 exit 1 } GLIBC_BC_POOL="${GLIBC_BC_POOL:-$GLIBC_BUILD/glibc-bc}" GLIBC_NATIVE_A="${GLIBC_NATIVE_A:-$GLIBC_BUILD/lib/libglibc_native.a}" # STATUS-CHANNEL crt selection (drive_abi=4; STATUS-CHANNEL-CONTRACT §10 crt # bullet — the glibc twin of libexec/brintcc-musl's selection): the SAME value-keyed # knob that selects abi-4 codegen selects the §4 terminus crt. An explicit # GLIBC_CRT override still wins. case "${HWJS_STATUS_CHANNEL:-}" in ''|0|1) ;; *) echo "wasmcc-glibc: FATAL: HWJS_STATUS_CHANNEL='${HWJS_STATUS_CHANNEL}' — this knob is VALUE-keyed (1 = on, 0 = off, unset/empty = default); refusing to guess (linux-in-the-browser#41)." >&2 exit 1 ;; esac # LANE-AWARE CHANNEL ADDRESSING (PLAN-THREADS-SINGLE-MEMORY §4 step 2) — the # glibc twin of libexec/brintcc-musl's guard: garbage HWJS_RT_LANES values and # lanes>1-without-the-channel refuse here too. The kernel packaging ships # the installed runtime LANED by default now (§4 step 4); a lanes>1 link # verifies that posture against the artifact's own symbol surface at the # link vantage below (a single-lane rebuild would alias other RT data # silently for every lane>=1, I3-2). The crt needs no laned twin (bitcode; # it inherits lane addressing in the merged pass run). WACKY_HWJS_RT (a # caller-built WIP RT) is the sanctioned escape, same as libexec/brintcc-musl. case "${HWJS_RT_LANES:-}" in ''|0|1) ;; *) if ! [[ "${HWJS_RT_LANES}" =~ ^[0-9]+$ && "${HWJS_RT_LANES}" -ge 2 && "${HWJS_RT_LANES}" -le 32 ]]; then echo "wasmcc-glibc: FATAL: HWJS_RT_LANES='${HWJS_RT_LANES}' — this knob is the lane COUNT: an integer 0..32 (unset/0/1 = single-lane; 2..32 = lane-aware channel addressing). Refusing to guess (PLAN-THREADS-SINGLE-MEMORY §4 step 2)." >&2 exit 1 fi if [[ "${HWJS_STATUS_CHANNEL:-0}" != "1" ]]; then echo "wasmcc-glibc: FATAL: HWJS_RT_LANES>1 without HWJS_STATUS_CHANNEL=1 — lane-aware addressing is a property of the status channel (drive_abi=4). Set both or neither (PLAN-THREADS-SINGLE-MEMORY §4 step 2)." >&2 exit 1 fi # Refusal (b) is LINK-vantage-only (a -c compile injects no RT): # recorded here, enforced below once is_link_mode is known. HWJS_LANES_LINK_GUARD=1 ;; esac if [[ -n "${GLIBC_CRT:-}" ]]; then echo "wasmcc-glibc: NOTE (CRT-PROVENANCE): explicit GLIBC_CRT override ($GLIBC_CRT) — WIP crt, provenance UNVERIFIED" >&2 elif [[ "${HWJS_STATUS_CHANNEL:-0}" == "1" ]]; then GLIBC_CRT="$GLIBC_BUILD/startfiles/crt1_glibc_hwjs_status.bc" else GLIBC_CRT="$GLIBC_BUILD/startfiles/crt1_glibc_hwjs.bc" fi # CRT-PROVENANCE (2026-07-21; glibc twin of libexec/brintcc-musl's check — the RT had # an attestation gate, the crt did not; a stale §4 terminus links silently). # Producer: build-glibc-libc.sh writes the sidecar. Verified whenever the # glibc source tree is present; explicit GLIBC_CRT override skips (noted). # # THE SOURCE IS RESOLVED FROM THIS CHECKOUT, for the reason written out at the # same function in libexec/brintcc-musl: it used to be spelled as a package # subpath under a super-repository, that path names nothing here, and "source # absent" meant "check skipped" — so the gate was off entirely and silently. # glibc is a submodule of this repository; BRINTIUM_GLIBC_SRC moves it. crt_provenance_verify() { [[ "$GLIBC_CRT" == "$GLIBC_BUILD/startfiles/"crt1_glibc_hwjs*.bc ]] || return 0 local glibc_src="${BRINTIUM_GLIBC_SRC:-$HWJS_HOME/glibc}" local crt_src="$glibc_src/sysdeps/wasm32/crt1-hwjs.c" if [[ ! -f "$crt_src" ]]; then echo "wasmcc-glibc: NOTE (CRT-PROVENANCE): NOT CHECKED — no glibc crt source at $crt_src, so the installed crt cannot be tied to any source. Point BRINTIUM_GLIBC_SRC at the glibc checkout, or run 'git submodule update --init glibc'." >&2 return 0 fi local crt_stamp="$GLIBC_CRT.stamp" if [[ ! -f "$crt_stamp" ]]; then echo "wasmcc-glibc: FATAL (CRT-PROVENANCE): no attestation sidecar at $crt_stamp — the installed crt cannot be tied to the checked-out glibc crt source. Rebuild: 'make -C sysroot glibc-libc'." >&2 exit 1 fi local crt_key crt_line crt_key="$(sha256sum "$crt_src" | cut -d' ' -f1)" crt_line="$(cat "$crt_stamp")" if [[ "$crt_line" != "CRT-PROVENANCE src=$crt_key" ]]; then echo "wasmcc-glibc: FATAL (CRT-PROVENANCE): the INSTALLED crt is STALE: $crt_stamp attests '$crt_line' but the checked-out crt source ($crt_src) keys to src=$crt_key — rebuild via 'make -C sysroot glibc-libc'. Refusing at the injection vantage." >&2 exit 1 fi } # ---- coro substrate startfiles (shared with musl) -------------------------- # PLAN-HWJS-RT-PSABI R3: the runtime TU is KERNEL-owned; the default-path # artifact must carry a verified RT-PROVENANCE sidecar (same contract as # libexec/brintcc-musl — see hwjs_rt_verify there). Explicit WACKY_HWJS_RT override # = WIP-runtime dev knob, honored with a loud NOTE. if [[ -n "${WACKY_HWJS_RT:-}" ]]; then echo "wasmcc-glibc: NOTE (RT-PROVENANCE): explicit WACKY_HWJS_RT override ($WACKY_HWJS_RT) — WIP runtime, provenance UNVERIFIED" >&2 HWJS_RT="$WACKY_HWJS_RT" HWJS_RT_VERIFY=0 else HWJS_RT="$TOOLS_DIR/build/hwjs_user_rt.bc" HWJS_RT_VERIFY=1 fi # NARROWED KEY (2026-07-21): verified against the RT-input srckey (the # ONE shared definition, linux scripts/hwjs-rt-srckey.sh), not the whole # linux HEAD — mirror of libexec/brintcc-musl hwjs_rt_verify, see the rationale # there. hwjs_rt_verify() { local rt="$1" stamp="$1.stamp" # Same pointer, TAKEN AND NOT COMPUTED, and the same situations, as # libexec/brintcc-musl — see the note there for what the removed fallback # cost. local linux_src="${BRINTIUM_KERNEL_SRC:-}" [[ -n "$linux_src" ]] || { echo "wasmcc-glibc: FATAL (RT-PROVENANCE): BRINTIUM_KERNEL_SRC is not set, and this link injects the kernel-owned runtime $rt, whose attestation can only be re-keyed by running the kernel's own scripts/hwjs-rt-srckey.sh over the kernel's own sources. This toolchain has no kernel of its own and there is nothing here to guess from. Set BRINTIUM_KERNEL_SRC to the kernel checkout that produced this runtime, or set WACKY_HWJS_RT to take the artifact verbatim, which is honoured and says on every link that its provenance is unverified." >&2 exit 1 } [[ -f "$stamp" ]] || { echo "wasmcc-glibc: FATAL (RT-PROVENANCE): no attestation sidecar at $stamp — the kernel owns hwjs_user_rt and attests it at the kernel link; rebuild it there (or override WACKY_HWJS_RT explicitly for a work-in-progress runtime)" >&2 exit 1 } [[ -d "$linux_src" ]] || { echo "wasmcc-glibc: FATAL (RT-PROVENANCE): BRINTIUM_KERNEL_SRC='$linux_src' is not a directory, so the attestation beside $rt cannot be re-keyed against the kernel it claims to come from. Point BRINTIUM_KERNEL_SRC at the kernel checkout that produced this runtime, or set WACKY_HWJS_RT to take the artifact verbatim — which is honoured, and says on every link that its provenance is unverified." >&2 exit 1 } local line; line="$(cat "$stamp")" local keyscript="$linux_src/scripts/hwjs-rt-srckey.sh" [[ -x "$keyscript" ]] || { echo "wasmcc-glibc: FATAL (RT-PROVENANCE): $keyscript is missing or not executable, and it is the one definition of the key this sidecar is written against. Either BRINTIUM_KERNEL_SRC='$linux_src' is not a kernel checkout, or it sits on a commit that predates the script; move it to one that carries it." >&2 exit 1 } local out; out="$("$keyscript" "$linux_src")" || { echo "wasmcc-glibc: FATAL (RT-PROVENANCE): hwjs-rt-srckey.sh failed for $linux_src — cannot verify $stamp" >&2 exit 1 } local key="${out%% *}" dirt="${out##* }" [[ "$dirt" == "0" ]] || { echo "wasmcc-glibc: FATAL (RT-PROVENANCE): the LINUX CHECKOUT carries uncommitted edits to the RT inputs (git -C $linux_src status -- arch/wasm32/vdso scripts/build-hwjs-rt.sh scripts/wasm-add-custom-section.py scripts/hwjs-rt-srckey.sh) — no attestation can cover uncommitted inputs; commit or revert, then 'make kernel FORCE=1'. (Pathspec-limited: edits elsewhere in the linux tree do NOT trip this.)" >&2 exit 1 } if [[ ! "$line" =~ ^RT-PROVENANCE\ commit=[0-9a-f]{40}\ dirty=0\ src=[0-9a-f]{64}\ srckey=$key$ ]]; then if [[ "$line" =~ \ srckey=[0-9a-f]{64}$ ]]; then echo "wasmcc-glibc: FATAL (RT-PROVENANCE): the INSTALLED RUNTIME ARTIFACT is STALE (or attests dirty RT inputs): $stamp attests '$line' but the checked-out linux RT inputs key to srckey=$key — re-attest via 'make kernel FORCE=1' then 'make -C sysroot hwjs-startfiles'. Refusing at the injection vantage." >&2 else echo "wasmcc-glibc: FATAL (RT-PROVENANCE): the INSTALLED RUNTIME ARTIFACT predates the narrowed RT-provenance key (no srckey= field): $stamp attests '$line' — the PRODUCER side is stale; rebuild via 'make kernel FORCE=1' then 'make -C sysroot hwjs-startfiles'. Refusing at the injection vantage." >&2 fi exit 1 fi } HWJS_ENTRY="${WACKY_HWJS_ENTRY:-$TOOLS_DIR/build/hwjs_user_entry.bc}" HWJS_THUNKS="${WACKY_HWJS_THUNKS:-$TOOLS_DIR/build/hwjs_user_thunks.o}" HWJS_WTHR="${WACKY_HWJS_WTHR:-$TOOLS_DIR/build/wasm_thread.k7hwjs.o}" BUILTINS="${WACKY_BUILTINS:-$TOOLS_DIR/build/libclang_rt.builtins-wasm32.a}" # ---- the tree this driver's linker must consume --------------------------- # NO PLUGIN PATH IS NAMED HERE. This driver used to decide the plugin home # itself, and so did half a dozen other files, each differently: this one # listed three plugins, one sibling listed none, and a fourth plugin was in # nobody's list at all. The linker derives the plugin home from its own # location now, and takes the libc pool, the startfiles and the builtins from # HWJS_TOOLS_DIR — which is the same pointer TOOLS_DIR above already resolved. # Explicit WASMLD_HWJS_*_PLUGIN / WASMLD_HWJS_SRC_DIR still win. # # THE EXPORT IS UNCONDITIONAL. It used to be guarded by "does a super-repo # layout exist", which is a question about a directory arrangement this # toolchain no longer has — so the guard is now always false, and the linker # would fall back to its own default for the pool while this driver resolved # the headers and startfiles from TOOLS_DIR. Two answers to one question is # the whole failure this pointer exists to remove: a link whose headers come # from one libc tree and whose bitcode comes from another produces an image # that loads and runs until it doesn't. Whatever tree this driver reads, its # linker reads. export WASMLD_HWJS=1 export HWJS_TOOLS_DIR="$TOOLS_DIR" export WASMLD_HWJS_LIBC_BC="${WASMLD_HWJS_LIBC_BC:-$GLIBC_BC_POOL}" # B-1.e loader decouple DCE (prunes runtime-dead ELF loader/locale) + keep the # dlopen/fork host-ABI roots public (so the dynamic RED arm's _Fork etc. export). export WASMLD_HWJS_DCE_MERGE="${WASMLD_HWJS_DCE_MERGE:-1}" # _Fork: the dlopen/fork host-ABI root (RED arm). wcslen: bash/coreutils # address-take the WEAK alias `wcslen`->`__wcslen`; without it as a DCE root the # internalize+globaldce pre-pass drops the alias (keeping only local `__wcslen`), # so `wcslen` leaks as a missing env import at real-kernel instantiate time (the # kernel-less harness stopgapped it with a JS import; the real kernel does not). # bcmp: same shape as wcslen — it is a WEAK alias -> `memcmp` (string_memcmp.bc, # force-included above). clang lowers `memcmp(a,b,n)==0` to a `bcmp` libcall, so # every bash/coreutils TU references `bcmp`; force-including the TU pulls the weak # alias in, but the internalize+globaldce pre-pass drops it unless it is ALSO a # DCE root, so it must be listed here too (else it leaks env.bcmp at instantiate). export WASMLD_HWJS_DCE_EXTRA_ROOTS="${WASMLD_HWJS_DCE_EXTRA_ROOTS:-_Fork,wcslen,bcmp}" # DYNAMIC_MAIN default ON for glibc userland: bash/coreutils are dlopen-capable # dynamic mains (import the shared __indirect_function_table) so the B-2 RED arm # (a real .so loaded through _dl_open at runtime) is exercised. Override =0 for a # self-contained static image. export WASMLD_DYNAMIC_MAIN="${WASMLD_DYNAMIC_MAIN:-1}" export WASMLD_DYNAMIC_MODULE="${WASMLD_DYNAMIC_MODULE:-1}" # ── BrintOS wasm target triples (hwjs vendor: the prescriptive coroutinized # platform). COMPILE uses a *-linux-gnu triple so glibc's headers see __linux__; # clang's DRIVER can only LINK a wasm32-*-unknown triple, so LINK uses -unknown. COMPILE_TARGET="--target=wasm32-hwjs-linux-gnu" LINK_TARGET="--target=wasm32-hwjs-unknown" # Tell wasmld (reached via -fuse-ld below) which COMPILE triple the program # bitcode carries, so the helper bitcode it synthesizes (abi_keepalive/main_bridge) # is stamped with the SAME -gnu triple and llvm-link sees one triple — no "Linking # two modules of different target triples" warning. export WASMLD_HWJS_COMPILE_TARGET="${WASMLD_HWJS_COMPILE_TARGET:-wasm32-hwjs-linux-gnu}" # PIC MODEL (glibc arm): unlike the musl arm, the glibc port has ALWAYS built PIC # — its pool/program/crt were compiled with a *-linux triple (wasm32-unknown-linux # -> wasm32-hwjs-linux-gnu, both PIC-default) and the wasm32 glibc userland is # proven booting that way. Keep PIC here (do NOT add -fno-pic like wasmcc's musl # arm) so program TUs match the PIC glibc bitcode pool built by ccw-glibc. CORO_FEATURES=( -matomics -mbulk-memory -mmutable-globals -msign-ext -mnontrapping-fptoint ) COMMON_INC=( -nostdlibinc -isystem "$GLIBC_SYSINC" -isystem "$GLIBC_UAPI_INC" ) # ---- phase detection (mirror wasmcc) --------------------------------------- is_link_mode=1 has_asm_input=0 # A -shared link builds a SIDE MODULE, not a program image (see the injection # note at the link line below). # A FINISHED side module, by content -- never by name. readline's build names # its object files readline.so, vi_mode.so, funmap.so ...; those are bitcode, # not shared libraries, and dropping them leaves the -shared link with no # inputs at all ("no bitcode roots on the link line"). Test the wasm magic and # the dylink.0 section instead, which is the same question wasmld's own # classifier asks. _hwjs_is_side_module() { [[ -f "$1" ]] || return 1 [[ "$(head -c4 "$1" 2>/dev/null | od -An -tx1 | tr -d ' \n')" == "0061736d" ]] || return 1 head -c 4096 "$1" 2>/dev/null | grep -qa 'dylink.0' } is_shared=0 for a in "$@"; do case "$a" in -c|-E|-S|-M|-MM|-fsyntax-only|--version|-v|-dumpversion|-dumpmachine|-print-*) is_link_mode=0 ;; -shared|--shared) is_shared=1 ;; *.s|*.S) has_asm_input=1 ;; esac done # A real wasmcc reports a single multilib dir "." like gcc (glibc's make rules # check ifneq ($(multidir),.)); clang-wasm prints "", which breaks them. for a in "$@"; do case "$a" in -print-multi-directory) echo "."; exit 0;; esac; done # Lanes>1 link posture check (PLAN-THREADS-SINGLE-MEMORY §4 step 4; mirror # of libexec/brintcc-musl): the kernel packaging ships the DEFAULT hwjs_user_rt.bc # laned now, so a lane-stamped link over the installed RT is the SUPPORTED # path — verified against the artifact's own symbol surface (the laned RT # DEFINES __hwjs_status_channel_lanes). A single-lane rebuild under a # lanes>1 link would pair lane-global checks with a one-element channel # array — every lane>=1 address silently aliases other RT data (I3-2). # Explicit WACKY_HWJS_RT overrides are exempt (WIP-runtime escape, noted # loudly above). # # SIGPIPE DISCIPLINE — NOT a pipe. See the long note on the same guard in # libexec/brintcc-musl: `nm | grep -q` under `set -o pipefail` turns a SUCCESSFUL match # into a FATAL whenever grep's early close SIGPIPEs llvm-nm, intermittently # and by construction. Capture, then match. An unreadable artifact is now a # DISTINCT verdict from a single-lane one. if [[ "$is_link_mode" == "1" && "${HWJS_LANES_LINK_GUARD:-0}" == "1" && "${HWJS_RT_VERIFY:-0}" == "1" ]]; then _rt_nm_rc=0 _rt_syms="$("$HWJS_LLVM_BIN/llvm-nm" --defined-only "$HWJS_RT" 2>&1)" || _rt_nm_rc=$? if [[ "$_rt_nm_rc" != "0" ]]; then echo "wasmcc-glibc: FATAL: HWJS_RT_LANES=${HWJS_RT_LANES} lane-posture check could NOT READ the installed runtime ($HWJS_RT) — llvm-nm exited $_rt_nm_rc: ${_rt_syms//$'\n'/ | }. This is not a lane verdict: the artifact is missing or unreadable. Rebuild the kernel packaging ('make kernel FORCE=1', then 'make -C sysroot hwjs-startfiles')." >&2 exit 1 fi if ! grep -q ' __hwjs_status_channel_lanes$' <<<"$_rt_syms"; then echo "wasmcc-glibc: FATAL: HWJS_RT_LANES=${HWJS_RT_LANES} on a LINK injecting the INSTALLED runtime ($HWJS_RT), but that artifact is SINGLE-LANE (it defines no __hwjs_status_channel_lanes array) — lane-global checks over a one-element channel array alias other RT data silently for every lane>=1 (I3-2). Rebuild the kernel packaging laned ('make kernel FORCE=1' with the default HWJS_RT_LANES_BUILD, then 'make -C sysroot hwjs-startfiles'), provide a caller-built laned runtime via WACKY_HWJS_RT, or drop the knob." >&2 exit 1 fi unset _rt_syms _rt_nm_rc fi if [[ "$is_link_mode" == "1" ]]; then crt_provenance_verify if [[ "${WASMLD_HWJS_CONFTEST:-0}" != "0" ]]; then # ── FAST CONFIGURE PATH (see wasmcc). autoconf link probes only check # that a link SUCCEEDS; the coro arm links with --allow-undefined anyway, # so a plain wasm-ld link is behaviour-equivalent for configure's yes/no # answers while skipping the whole-program glibc-pool merge + coroutinize # pass entirely. Two-phase because glibc COMPILES with wasm32-hwjs-linux-gnu # (headers need __linux__) but LINKS with wasm32-hwjs-unknown (clang has # no linux wasm link target): compile each source to a temp object with the # glibc headers, then plain wasm-ld link. NEVER set for a real build. cft_stage="$(mktemp -d "${TMPDIR:-/tmp}/wasmcc-glibc-cft.XXXXXX")" trap 'rm -rf "$cft_stage"' EXIT # HONEST FUNCTION PROBES (hwjs-cc#8, the glibc twin of ticket #4). # This path links with --allow-undefined and NO libc, so every # undefined symbol — real glibc function or pure fiction — became an # env.* import and the link "succeeded": AC_CHECK_FUNCS answered yes # for EVERYTHING on this arm (xz's HAVE_PLEDGE — an OpenBSD-only # function glibc does not provide). The musl arm already attests the # probe image with the PHANTOM-IMPORT witness; mirror it here: after # the fast link, judge the image's imports against an allowlist of # every symbol the REAL link would resolve (the glibc bitcode pool + # glibc coro startfile + coro substrate + native .S archive + # compiler-rt builtins), cached next to the pool and rebuilt whenever # any provider is newer. A probe calling a real glibc function still # answers yes; a probe calling a NONEXISTENT function now fails the # link — configure's yes/no matches what a real build can provide. # # ARCHIVE LINK-PROBES (linux-in-the-browser#63, same cost class as the # musl arm): a resolvable static archive on a conftest line would drag # its bitcode members through plain wasm-ld's LTO codegen on EVERY # probe (~30s each against a multi-MB archive). Take resolvable # archives OFF the fast link line and let each speak through its # externally-defined llvm-nm symbol list instead (cached per archive, # keyed on mtime+size, same symcache as the musl arm), merged into # the witness allowlist — probe answers are unchanged in both # directions without the per-probe archive-LTO cost. An UNRESOLVABLE # -lNAME rides to the LINK and fails it honestly (AC_CHECK_LIB "no" # for a nonexistent library) — before #63 it landed in the # compile-flag bucket and never reached the link at all, so a probe # for a library that does not exist answered "yes" on this arm. cft_Ldirs=(); cft_prev="" for a in "$@"; do case "$a" in -L?*) cft_Ldirs+=( "${a#-L}" ) ;; esac [[ "$cft_prev" == "-L" ]] && cft_Ldirs+=( "$a" ) cft_prev="$a" done cft_out="a.out"; cft_cflags=(); cft_srcs=(); cft_inputs=(); cft_prev="" cft_archives=(); cft_seen_archives="" cft_note_archive() { local abs abs="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" case "$cft_seen_archives" in *"|$abs|"*) return ;; esac cft_seen_archives="$cft_seen_archives|$abs|" cft_archives+=( "$abs" ) } for a in "$@"; do if [[ "$cft_prev" == "-o" ]]; then cft_out="$a"; cft_prev=""; continue; fi case "$a" in -o) cft_prev="-o"; continue ;; -o*) cft_out="${a#-o}" ;; *.c|*.cc|*.cpp|*.cxx|*.C|*.cp|*.c++) cft_srcs+=( "$a" ) ;; *.a) if [[ -f "$a" ]]; then cft_note_archive "$a"; else cft_inputs+=( "$a" ); fi ;; # The glibc-pool-provided set — the REAL link (below) drops # exactly these, so the probe must answer "yes" for them too # (the pool allowlist speaks for their symbols). -lc|-lc_nonshared|-lpthread|-lm|-ldl|-lrt|-lresolv|-lutil|-lcrypt) : ;; -l?*) cft_name="${a#-l}"; cft_found="" for d in "${cft_Ldirs[@]}"; do [[ -f "$d/lib${cft_name}.a" ]] && { cft_found="$d/lib${cft_name}.a"; break; } done # Resolvable -> off the link line, speaks through its nm # symbol list (see above); unresolvable -> the LINK line, # where wasm-ld fails it loudly. if [[ -n "$cft_found" ]]; then cft_note_archive "$cft_found"; else cft_inputs+=( "$a" ); fi ;; *.o) cft_inputs+=( "$a" ) ;; -*) cft_cflags+=( "$a" ) ;; *) cft_inputs+=( "$a" ) ;; esac cft_prev="" done for s in "${cft_srcs[@]}"; do o="$cft_stage/$(basename "$s").o" "$CLANG" $COMPILE_TARGET "${COMMON_INC[@]}" "${CORO_FEATURES[@]}" \ -fno-builtin -fvisibility=default -ffunction-sections -fdata-sections \ "${cft_cflags[@]}" -c "$s" -o "$o" || exit $? cft_inputs+=( "$o" ) done # ── THE ALLOWLIST IS BUILT BEFORE THE LINK, because the link now USES it. # # hwjs-cc#8 made FUNCTION probes honest here (the musl arm's ticket-#4 attestation with # glibc providers): the fast link imports every undefined function and the PHANTOM-IMPORT # witness afterwards refuses any name no real link could resolve. DATA probes were left # behind for a structural reason — --allow-undefined binds an undefined DATUM to address 0 # and emits NO import, so a witness that reads the import section cannot see it. Every # autoconf link-probe for a VARIABLE therefore answered YES: AC_STRUCT_TIMEZONE's tzname, # sys_siglist, program_invocation_name, any `extern int foo;` a package tests for. The # package then compiled a reference to address 0 into its shipped binary and skipped the # fallback it keeps for exactly this case. # # Splitting --allow-undefined into its two halves (lld Driver.cpp:865-870: # --import-undefined + --unresolved-symbols=ignore) fixes it with the allowlist already # here and already derived: undefined FUNCTIONS still import (the #8 flow is bit-for-bit # unchanged), an undefined DATUM links iff the glibc pool / crt / native archive / # builtins / line archives actually define it (honest yes), and anything else is # `undefined symbol: ` (honest no). No symbol name is written down; the list is # llvm-nm over the providers, rebuilt whenever a provider is newer. # # lld honors only the LAST --allow-undefined-file= (Driver.cpp getLastArg), so the pool # list and the per-archive lists are concatenated into ONE file — a second flag would # silently shadow the first. allowlist="$GLIBC_BC_POOL/.conftest-resolvable-symbols.txt" providers=( "$GLIBC_BC_POOL"/*.bc "$GLIBC_CRT" "$HWJS_RT" "$HWJS_ENTRY" "$HWJS_THUNKS" "$HWJS_WTHR" "$GLIBC_NATIVE_A" ) [[ -f "$BUILTINS" ]] && providers+=( "$BUILTINS" ) # The program link takes the bitcode TU in the archive's place; it also # carries the complex helpers the archive lacks. _pic_bi="${WASMLD_HWJS_LIBC_BUILTINS_BC:-$TOOLS_DIR/build/builtins-wasm32-pic.bc}" [[ -f "$_pic_bi" ]] && providers+=( "$_pic_bi" ) rebuild=0 if [[ ! -f "$allowlist" ]]; then rebuild=1 else for p in "${providers[@]}"; do [[ -f "$p" && "$p" -nt "$allowlist" ]] && { rebuild=1; break; } done fi if [[ "$rebuild" == "1" ]]; then if ! "$HWJS_LLVM_BIN/llvm-nm" --defined-only "${providers[@]}" 2>/dev/null \ | awk '{print $NF}' | sort -u > "$allowlist.tmp.$$"; then rm -f "$allowlist.tmp.$$" echo "wasmcc-glibc: FATAL (conftest): llvm-nm over the glibc pool failed — cannot build the resolvable-symbol allowlist, so the probe's import surface cannot be attested. Refusing rather than answering configure dishonestly." >&2 exit 1 fi mv -f "$allowlist.tmp.$$" "$allowlist" fi # Per-archive symbol allowlists (linux-in-the-browser#63): each archive # taken off the link line speaks through its EXTERNALLY-DEFINED symbol # list. Same cache + key scheme as the musl arm (basename + path hash + # mtime + size); only external definitions are allowlisted — an # archive-local 't' symbol cannot resolve a probe's reference in a # real link, so it must not here either. cft_witness_allow=( --allow-undefined-from="$allowlist" ) cft_allow_files=( "$allowlist" ) if [[ "${#cft_archives[@]}" -gt 0 ]]; then cft_symcache="${WASMLD_HWJS_CONFTEST_SYMCACHE:-${TMPDIR:-/tmp}/hwjs-conftest-symcache-$(id -u)}" mkdir -p "$cft_symcache" for ar_path in "${cft_archives[@]}"; do ar_stat="$(stat -c '%Y.%s' "$ar_path" 2>/dev/null)" || { echo "wasmcc-glibc: FATAL (conftest): cannot stat archive '$ar_path' — its symbol surface cannot be attested. Refusing rather than answering configure dishonestly." >&2 exit 1 } ar_key="$(printf '%s' "$ar_path" | cksum | awk '{print $1}')" ar_syms="$cft_symcache/$(basename "$ar_path").$ar_key.$ar_stat.syms" if [[ ! -f "$ar_syms" ]]; then if ! "$HWJS_LLVM_BIN/llvm-nm" --defined-only "$ar_path" 2>/dev/null \ | awk 'NF >= 2 && $(NF-1) ~ /^[A-Z]$/ { print $NF }' | sort -u > "$ar_syms.tmp.$$"; then rm -f "$ar_syms.tmp.$$" echo "wasmcc-glibc: FATAL (conftest): llvm-nm over archive '$ar_path' failed — its symbol surface cannot be attested, so the probe's yes/no would be a guess. Refusing rather than answering configure dishonestly." >&2 exit 1 fi mv -f "$ar_syms.tmp.$$" "$ar_syms" fi cft_witness_allow+=( --allow-undefined-from="$ar_syms" ) cft_allow_files+=( "$ar_syms" ) done fi # The ONE file lld will read (see the note above on getLastArg). cft_undef_allow="$(mktemp "${TMPDIR:-/tmp}/hwjs-conftest-undef.XXXXXX")" # Both, not just the new one: this branch already armed a trap for $cft_stage above, and a # bare re-trap would REPLACE it and leak the staged objects on every probe. trap 'rm -rf "$cft_stage"; rm -f "$cft_undef_allow"' EXIT cat "${cft_allow_files[@]}" > "$cft_undef_allow" || { echo "wasmcc-glibc: FATAL (conftest): cannot assemble the resolvable-symbol list for the link — without it an undefined DATUM would bind to address 0 unnoticed and the probe would answer configure 'yes' for a variable nothing defines. Refusing." >&2 exit 1 } # --export-all keeps the whole probe LIVE: with --no-entry and no # exports wasm-ld garbage-collects everything (main included), so the # probe's undefined references would never surface as imports and the # witness below would see an empty image (which is exactly how the # pre-#8 pledge probe "passed": the image had no imports to judge and # no judge). The image is never run; only the yes/no matters. # --error-limit=0: lld stops naming undefined symbols after 20, and a # probe that reports two of its five missing names is a diagnostic that # runs out halfway. hwjs_link "$CLANG" "$HWJS_LLVM_BIN/wasm-ld" \ $LINK_TARGET -nostdlib "${CORO_FEATURES[@]}" \ -Wl,--no-entry -Wl,--import-undefined \ -Wl,--allow-undefined-file="$cft_undef_allow" \ -Wl,--error-limit=0 -Wl,--export-all \ "${cft_inputs[@]}" -o "$cft_out" || exit $? [[ -f "$cft_out" ]] || exit 0 # An installed toolchain's own witness; see libexec/brintld. if [[ -x "$HWJS_HOME/libexec/brint-tool" ]]; then IMPORT_WITNESS=( "$HWJS_HOME/libexec/brint-tool" import-witness ) else IMPORT_WITNESS=( node "$HWJS_HOME/libexec/wasm-import-witness.mjs" ) if [[ ! -f "${IMPORT_WITNESS[1]}" ]] || ! command -v node >/dev/null 2>&1; then echo "wasmcc-glibc: FATAL (conftest): the PHANTOM-IMPORT witness (libexec/wasm-import-witness.mjs + node) is unavailable — the probe's import surface cannot be attested. Refusing rather than answering configure dishonestly." >&2 exit 1 fi fi phantom="$("${IMPORT_WITNESS[@]}" "$cft_out" --mode=static "${cft_witness_allow[@]}" 2>&1)" && exit 0 echo "$phantom" | sed 's/^/wasmcc-glibc: conftest probe references unresolvable symbol(s): /' >&2 echo "wasmcc-glibc: conftest link FAILED honestly (WASMLD_HWJS_CONFTEST fast path, hwjs-cc#8): the probe references symbol(s) the wasm32 glibc does not provide — answering configure 'no' instead of shipping a phantom env import." >&2 rm -f "$cft_out" exit 1 fi # ── Dynamic-libc consumer arms (mirror wasmcc's musl arm; PLAN-DYNAMIC-LIBC). # WASMLD_RT_IMPORT=1 (PLAN-HWJS-RT-PSABI R4): the kernel-provided libhwjs-rt # (module #0) is the runtime provider at load time — the runtime TU stays OUT # of this merge and the main IMPORTS the §11 op surface (the witness sanctions # exactly that). WASMLD_LIBC_IMPORT=1 (PLAN-DYNAMIC-LIBC): libc.so # (module #1) is the C-runtime provider — the glibc bitcode pool is NOT pulled # (wasmld's LIBC_IMPORT arm swaps --unresolved-symbols=import-dynamic and binds # the main's libc refs to libc.so by name), so the main carries NO libc of its # own: the native glibc pieces this arm normally force-includes (the wasm32 # syscall helper, the weak-alias TUs, the native .S syscall/setup archive # libglibc_native.a) all live in libc.so and MUST be dropped here, exactly as # the musl fixture main carries none of the musl pool. RT_IMPORT="${WASMLD_RT_IMPORT:-0}" LIBC_IMPORT="${WASMLD_LIBC_IMPORT:-0}" if [[ "$RT_IMPORT" == "1" && "${WASMLD_DYNAMIC_MAIN:-0}" == "0" ]]; then echo "wasmcc-glibc: FATAL (WASMLD_RT_IMPORT): runtime-import linking requires WASMLD_DYNAMIC_MAIN=1 (a static image must carry the archive packaging). Mirror of the musl wasmcc guard." >&2 exit 1 fi if [[ "$LIBC_IMPORT" == "1" && "${WASMLD_DYNAMIC_MAIN:-0}" == "0" ]]; then echo "wasmcc-glibc: FATAL (WASMLD_LIBC_IMPORT): a libc.so-importing main is a dynamic main by definition (imports the shared table; the loader binds module #1) — set WASMLD_DYNAMIC_MAIN=1. Mirror of wasmld's guard." >&2 exit 1 fi need_f=( "$GLIBC_CRT" "$HWJS_RT" "$HWJS_ENTRY" "$HWJS_THUNKS" "$HWJS_WTHR" "$GLIBC_NATIVE_A" ) [[ "$is_shared" == "1" ]] && need_f=() for f in "${need_f[@]}"; do [[ -f "$f" ]] || { echo "wasmcc-glibc: LINK needs '$f' (build the glibc B-2 rig: make -C distros/linux-6.12-glibc-bash-coreutils glibc-libc)" >&2; exit 1; } done compgen -G "$GLIBC_BC_POOL/*.bc" >/dev/null || { echo "wasmcc-glibc: empty glibc bitcode pool at $GLIBC_BC_POOL" >&2; exit 1; } # Stage the bitcode startfiles under .o names so clang hands them to wasmld # untouched (wasmld classifies by file magic, not extension) — same §8b # delivery the musl wasmcc does. stage="$(mktemp -d "${TMPDIR:-/tmp}/wasmcc-glibc.XXXXXX")" trap 'rm -rf "$stage"' EXIT stage_bc(){ local b; b="$(basename "${1%.bc}")"; cp -f "$1" "$stage/$b.o"; printf '%s\n' "$stage/$b.o"; } CRT_O="$(stage_bc "$GLIBC_CRT")" # R3: verify the kernel attestation at the injection vantage. [[ "${HWJS_RT_VERIFY:-0}" == "1" ]] && hwjs_rt_verify "$HWJS_RT" # RT_IMPORT arm (mirror wasmcc lines ~491-556): exclude the runtime TU from # the merge and inject the publication-seed TU (annotated __hwjs_user_park # prototype) so the excluded runtime TU's hwjs.suspends.v1 seed for the one # userland park leaf is restored — else glibc's dlopen/_Fork/execve callers # stay native and rt's leaf coroutine parks under a native caller (the F151 # orphan-at-clear trap). RED: HWJS_RT_IMPORT_NO_SEED=1 reproduces it. RT_O="" RT_SEED_O="" if [[ "$RT_IMPORT" == "1" ]]; then echo "wasmcc-glibc: NOTE (WASMLD_RT_IMPORT): runtime TU excluded from the merge — the kernel-provided libhwjs-rt (module #0) is the provider at load time (PLAN-HWJS-RT-PSABI R4)." >&2 if [[ "${HWJS_RT_IMPORT_NO_SEED:-0}" != "0" ]]; then if [[ "${WASMLD_HWJS_ALLOW_RED_KNOBS:-0}" != "1" ]]; then echo "wasmcc-glibc: FATAL: fix-disabling RED knob HWJS_RT_IMPORT_NO_SEED=${HWJS_RT_IMPORT_NO_SEED} is set for a PRODUCTION rt-import link — without the publication-seed TU the runtime's park leaf __hwjs_user_park loses its hwjs.suspends.v1 seed and the leaf parks under a native caller (the in-guest orphan-at-clear trap). Unset it, or set WASMLD_HWJS_ALLOW_RED_KNOBS=1 for a deliberate non-vacuity arm." >&2 exit 1 fi echo "wasmcc-glibc: NOTE (HWJS_RT_IMPORT_NO_SEED, RED arm ACKNOWLEDGED): SKIPPING the rt-import publication-seed TU — the pre-fix park-under-native shape. Non-production." >&2 else RT_SEED_O="$stage/hwjs-rt-import-pubseed.o" cat > "$stage/hwjs-rt-import-pubseed.c" <<'PUBSEED' /* hwjs-rt-import publication seed — GENERATED by wasmcc-glibc (WASMLD_RT_IMPORT=1). * Annotated prototype for the rt callable surface's one SUSPENDING leaf, * re-published module-side because the runtime TU (its publishing home) is * excluded from this merge. See the injection comment in libexec/brintcc-glibc. */ __attribute__((annotate("hwjs.suspends.v1"))) extern void __hwjs_user_park(unsigned int reason); __attribute__((used)) static void __hwjs_rt_import_pubseed(void) { __hwjs_user_park(0u); /* reason 0 = NONE/breath; never executed */ } PUBSEED "$CLANG" $COMPILE_TARGET "${COMMON_INC[@]}" "${CORO_FEATURES[@]}" \ -fno-builtin -fvisibility=default \ -emit-llvm -c "$stage/hwjs-rt-import-pubseed.c" -o "$RT_SEED_O" \ || { echo "wasmcc-glibc: FATAL (WASMLD_RT_IMPORT): the publication-seed TU failed to compile to bitcode (error above) — an unseeded rt-import link would park under native callers, so refusing." >&2; exit 1; } echo "wasmcc-glibc: NOTE (WASMLD_RT_IMPORT): INJECTED the publication-seed TU — hwjs.suspends.v1 prototype for __hwjs_user_park (RED: HWJS_RT_IMPORT_NO_SEED=1)." >&2 fi else RT_O="$(stage_bc "$HWJS_RT")" fi ENTRY_O="$(stage_bc "$HWJS_ENTRY")" # The wasm32 syscall helper (__wasm32_syscall6/_ret/_errval) is referenced by # the NATIVE .S syscall stubs (libglibc_native.a) AND glibc's C syscall path. # The bitcode merge's --only-needed cannot see a native-object reference, so # force-include it (otherwise it stays an undefined env import). SYSCALL_O=() # Dynamic-libc arm (WASMLD_LIBC_IMPORT=1): the wasm32 syscall helper AND the weak-alias # force-includes below are libc PIECES — under the consumer arm they live in # libc.so (module #1) and the main imports them by name; force-including them # here would put a private copy of libc's syscall/setup code in a main that is # meant to carry none (and defeat the import-dynamic bind). Skip them, exactly # as the musl fixture main pulls no musl pool. if [[ "$LIBC_IMPORT" != "1" ]]; then SYSCALL_BC="${GLIBC_SYSCALL_BC:-$GLIBC_BC_POOL/misc_wasm-syscall.bc}" [[ -f "$SYSCALL_BC" ]] && SYSCALL_O=( "$(stage_bc "$SYSCALL_BC")" ) # Force-include pool TUs that provide a WEAK public alias which the bitcode # merge's --only-needed leaves behind. --only-needed treats a WEAK undefined # reference as already-satisfied (a weak undef is allowed to resolve to null), # so even when the module's STRONG symbol was pulled (e.g. __wcslen, __pthread_once, # _IO_fopen64, __fgets_unlocked) the co-resident weak PUBLIC alias (wcslen, # pthread_once, fopen64, fgets_unlocked) is never linked and leaks as an env # import at real-kernel instantiate. Force-including the whole TU as a root # (full-link, not --only-needed) brings the weak alias in; if a given binary # never references it, the internalize+globaldce pre-pass prunes it (dead weak # def), so simple utils are unaffected. # wcsmbs_wcslen -> wcslen (address-taken; also a DCE root, below) # libio_iofopen64 -> fopen64 (coreutils gnulib fopen path: ls, cat, sort, ...) # libio_iofgets_u -> fgets_unlocked (coreutils line readers) # nptl_pthread_once -> pthread_once (glibc/gnulib once-init public alias) # string_memcmp -> bcmp (WEAK alias to memcmp; clang lowers # `memcmp(a,b,n)==0` to a bcmp libcall, # so EVERY bash/coreutils TU emits an # undefined `bcmp` the --only-needed # merge treats as weak-satisfied and # never pulls -> leaks env.bcmp at # instantiate. Force the memcmp TU in.) # inet_if_index (__if_nametoindex, a STRONG symbol referenced by the pulled # inet_inet6_scopeid_pton on the NSS/getaddrinfo closure) is force-included in # the same list: it is a genuine strong undef whose definer sat one pull-pass # past the fixpoint window, so pin it as a root rather than deepen the pull. # A bounded list, never-silent: a new unforced leak shows up as a loud missing # import at instantiate, not a silent stub. for wk in wcsmbs_wcslen libio_iofopen64 libio_iofgets_u nptl_pthread_once string_memcmp inet_if_index; do [[ -f "$GLIBC_BC_POOL/$wk.bc" ]] && SYSCALL_O+=( "$(stage_bc "$GLIBC_BC_POOL/$wk.bc")" ) done fi # LIBC_IMPORT != 1 # Single-step (compile+link) support — the same fix wasmcc's musl arm landed # (HWJS-DEFERRALS §2 single-step gap, ticket #6 follow-up): a C-family source # on the LINK line must be pre-compiled to BITCODE with the COMPILE triple + # the glibc headers, exactly like compile mode, so it joins the merged-module # coroutinize pass. Handing it to clang's LINK driver instead compiles it # NATIVE under the -unknown triple with NO glibc include path — iproute2's # misc/Makefile one-step rules (nstat/ifstat/rtacct: `$(CC) ... -o nstat # nstat.c -lm`) then die at `'stdio.h' file not found` (the glibc-arm twin of # the musl gap). Collect the compile-relevant flags off the link line first. cc_flags=() cc_expect="" for a in "$@"; do if [[ -n "$cc_expect" ]]; then [[ "$cc_expect" == "cc" ]] && cc_flags+=( "$a" ) cc_expect="" continue fi case "$a" in -o|-L) cc_expect="skip" ;; -I|-D|-U|-include|-imacros|-isystem|-idirafter|-iquote|-Xclang) cc_flags+=( "$a" ); cc_expect="cc" ;; -o*|-L*|-l*|-Wl,*) : ;; -shared|--shared) : ;; -*) cc_flags+=( "$a" ) ;; *) : ;; esac done # Resolve -L then map each -lNAME -> /libNAME.a (wasmld can't infer # bitcode-vs-native from a name); drop -lc (wasmld owns the glibc pool). Ldirs=() # BRINTOS-884: libraries a `-lNAME` resolved to a SHARED object. Not link # inputs — needed-graph entries, handed to wasmld through the environment knob # it already reads, so one recorder derives every digest and soname. needed_sos=() # ON for the dynamic-main consumer arm AND for a -shared side module. On the # -shared arm the hits leave the link line and reach wasmld as # WASMLD_SHARED_SIBLING_LIBS (below), which records them in the side module's # own wasm.linux.needed (hwjs-cc#128). A static link cannot consume a .so. # Everywhere else -lNAME keeps resolving to lib.a, byte for byte as before. dash_l_shared=0 [[ ( "$LIBC_IMPORT" == "1" || "$is_shared" == "1" ) && "${WASMLD_DASH_L_STATIC:-0}" != "1" ]] && dash_l_shared=1 prev="" for a in "$@"; do case "$a" in -L?*) Ldirs+=( "${a#-L}" );; esac [[ "$prev" == "-L" ]] && Ldirs+=( "$a" ) prev="$a" done link_args=() prev="" srcn=0 for a in "$@"; do # OUTPUT_OPERAND_VERBATIM: `-o`'s operand is a filename to CREATE, not an # input to classify. The loop below sorts every argument by shape and, until # this arm, tracked `prev` for `-L` alone -- so the output path fell through # to the shape arms like any other token. A `-shared` link's output ends in # `.so` by definition, so it was tested by the `*.so` arm, and the answer # depended on whether the file happened to exist yet: # # do_compile, FIRST link -- no file, test false, path kept, link succeeds. # do_install, RE-link -- the file is there from do_compile and IS a # valid side module, so SIDE_MODULE_SO_DROP ate # the output path. # # `-o` was then left to take the NEXT token as its operand, which in waf's # link rule is STLIB_MARKER (`-Wl,-Bstatic`) -- and wasmld bound its output # name to that, which node then rejects as one of its own options. That # asymmetry is the whole of "do_compile succeeds, the install re-link does # not". # # The SEPARATED form above is the live defect. The attached `-o.so` # form reaches the same `*.so` arm, but survives today by ACCIDENT: # _hwjs_is_side_module opens its argument as a path, and `-o` is not # one, so the `-f` test fails and the token is kept. It is exempted here # too, so the output operand is safe by construction rather than by that # coincidence -- and stays safe if the existence test is ever relaxed. if [[ "$prev" == "-o" ]]; then link_args+=( "$a" ); prev=""; continue; fi if [[ "$prev" == "-L" ]]; then prev=""; continue; fi case "$a" in -o) link_args+=( "$a" ); prev="-o"; continue ;; -o?*) link_args+=( "$a" ); continue ;; -L) prev="-L"; continue ;; -L?*) continue ;; *.c|*.cc|*.cpp|*.cxx|*.C|*.cp|*.c++) # Single-step source: pre-compile to bitcode (see above) so it # joins the merged-module pass like a two-step object. if [[ "${HWJS_CORO_NO_LINK_SRC_BC:-0}" != "0" ]]; then echo "wasmcc-glibc: NOTE (HWJS_CORO_NO_LINK_SRC_BC, RED arm ACKNOWLEDGED): leaving link-line source '$a' to the native clang link driver — the pre-fix single-step behavior. Non-production." >&2 link_args+=( "$a" ) else [[ -f "$a" ]] || { echo "wasmcc-glibc: FATAL: link-line source '$a' does not exist." >&2; exit 1; } srcn=$((srcn + 1)) src_base="$(basename "$a")" src_bc="$stage/linksrc-${srcn}-${src_base%.*}.o" "$CLANG" $COMPILE_TARGET "${COMMON_INC[@]}" "${CORO_FEATURES[@]}" \ -fno-builtin -fvisibility=default -ffunction-sections -fdata-sections \ "${cc_flags[@]}" -emit-llvm -c "$a" -o "$src_bc" \ || { echo "wasmcc-glibc: FATAL: single-step bitcode compile of link-line source '$a' failed (it must join the merged-module coroutinize pass; error above)." >&2; exit 1; } link_args+=( "$src_bc" ) fi ;; -lc|-lc_nonshared|-lpthread|-lm|-ldl|-lrt|-lresolv|-lutil|-lcrypt) : ;; # all in the glibc pool -l?*) name="${a#-l}"; found=""; found_so="" # Per DIRECTORY, .so before .a — GNU ld's order, so a -L earlier # on the line shadows a later one whichever kind it holds. # DASH_L_SHARED_WASM_ONLY: the .so must BE a wasm module. ncurses # stages libtermcap.so as an ld script (`INPUT(libncurses.so)`), # and a non-wasm file recorded as a needed entry fatals the link # in needed_so_record. Rejecting here falls through to the # archive branch, then to the loud not-found. A -shared link # records nothing as a needed entry -- it drops the hit -- so the # test is scoped off there rather than turning that drop into a # refusal. # # BRINTOS-2689 NARROWED THIS. A -shared link still records no needed # entry, but it now READS these hits to derive the sibling suspend # seed, so "nothing is recorded" no longer means "nothing consumes # it". The scoping-off stays -- turning it into a refusal would fail # ncurses' libtermcap.so ld script, which is the case it was written # for -- and the content test moves to the EXPORT below instead, so a # non-module can still resolve and be dropped but can never enter the # seed. for d in "${Ldirs[@]}"; do [[ $dash_l_shared == 1 && -f "$d/lib${name}.so" ]] \ && { [[ $is_shared == 1 ]] || _hwjs_is_side_module "$d/lib${name}.so"; } \ && { found_so="$d/lib${name}.so"; break; } [[ -f "$d/lib${name}.a" ]] && { found="$d/lib${name}.a"; break; } done if [[ -n "$found_so" ]]; then # NOT a link input: handing the .so to the linker asks it to # absorb a dynamic object statically, which it refuses by name. sodir="$(cd "$(dirname "$found_so")" && pwd)" needed_sos+=( "$sodir/$(basename "$found_so")" ) elif [[ -n "$found" ]]; then fdir="$(cd "$(dirname "$found")" && pwd)"; link_args+=( "$fdir/$(basename "$found")" ); else link_args+=( "$a" ); fi ;; # PATH_SO_NEEDED: a shared object named by PATH rather than by -lNAME. # A shared object named by PATH rather than by -lNAME. libtool # always spells the path, so the arm above never sees a # libtool-driven consumer link and the .so fell through to # link_args, reaching wasm-ld as an ordinary input -- which it # cannot absorb. On the dynamic-main arm it belongs in the same # needed-graph the -l hits go to, derived by the same recorder. *.so|*.so.*) if [[ $dash_l_shared == 1 && -f "$a" ]] && _hwjs_is_side_module "$a"; then sodir="$(cd "$(dirname "$a")" && pwd)" needed_sos+=( "$sodir/$(basename "$a")" ) elif [[ "$is_shared" == "1" ]] && _hwjs_is_side_module "$a"; then # SIDE_MODULE_SO_DROP: a side module DEPENDING on another # library, which is not the same as absorbing one. wasmld # refuses a finished .so among -shared inputs because its # code cannot join the one merged-module pass run and its # hwjs.abi record would concatenate -- both about swallowing # the module, neither about the dependency. Reached only # under WASMLD_DASH_L_STATIC=1; otherwise the arm above # takes the hit and wasmld records it (hwjs-cc#128). : else link_args+=( "$a" ); fi ;; *) link_args+=( "$a" ) ;; esac prev="" done # Dynamic-libc arm (WASMLD_LIBC_IMPORT=1): the native .S syscall/setup archive is a libc # piece — it lives in libc.so under the consumer arm; drop it from the main. NATIVE_A=( "$GLIBC_NATIVE_A" ) [[ "$LIBC_IMPORT" == "1" ]] && NATIVE_A=() SUBSTRATE=( "$HWJS_THUNKS" "$HWJS_WTHR" "$BUILTINS" ) # SIDE MODULE (-shared): a .so is not a program. It has no _start, it carries # no libc of its own, and wasmld's side-module rule requires every input to # arrive as BITCODE — so the whole program-image injection set comes off the # line: the crt/entry/runtime startfiles (whose symbols belong to the main) # and the four NATIVE pieces wasmld names in its FATAL (libglibc_native.a, # hwjs_user_thunks.o, wasm_thread.k7hwjs.o, libclang_rt.builtins-wasm32.a). # The library's libc references stay undefined and become env.* dynamic # imports the loader binds to libc.so — the same shape wasmld -shared # produces when driven directly (test/m5-libc-so-roundtrip.sh). Without this # arm `$CC -shared` — what libtool, autotools and debian/rules emit — cannot # build a shared library on this arm at all, while `wasmld -shared` can. SHARED_LD=() if [[ "$is_shared" == "1" ]]; then CRT_O=""; RT_O=""; RT_SEED_O=""; ENTRY_O="" SYSCALL_O=(); NATIVE_A=(); SUBSTRATE=() # A side module has no entry point. clang's wasm driver still asks the # linker for one on this vendor triple (it only implies --no-entry for # the triples it recognises as -shared-capable), so say it explicitly. SHARED_LD=( -Wl,--no-entry ) # The B-1.e loader-decouple DCE internalizes everything unreachable from # a PROGRAM's roots. A side module has no entry, so it internalizes the # library itself away: the .so links and is valid, but exports only # __wasm_call_ctors and has lost every libc reference — a linking EMPTY # object, the worst kind of false green. Off for -shared; the roots it # keeps (_Fork, wcslen, bcmp) are main-image concerns. export WASMLD_HWJS_DCE_MERGE=0 # A side module is NOT a main, so the dynamic-libc MAIN-consumer arm must # not ride this link. It arrives as environment (a distro that links its # mains against libc.so exports it for the whole build task), and wasmld # refuses it on -shared by design: "a side module's libc imports bind # through the loader's needed-graph, not through the main consumer arm". # Without this, enabling shared libraries and dynamic libc at the same # time makes every package that builds a .so fail that guard. unset WASMLD_LIBC_IMPORT WASMLD_RT_IMPORT WASMLD_LIBC_SO_PATH # The partition is NOT part of that arm: it describes libc.so's exports, and a # side module's own libc calls need the same per-export verdicts a main's do. # Dropped, the .so links partition-less and every cross-module import falls to # attribute keying -- which on a plain decl means NATIVE -- so a # coroutinized libc callee parks under a call the library never bracketed. # Re-spell into the third-.so consumer arm rather than discarding # (PLAN-DYNAMIC-LIBC §3.2); the SUSPENDERS spelling would be read by the # names-only parser, seeding every 'native' verdict line may-suspend. if [[ -z "${WASMLD_HWJS_XMOD_PARTITION_LIBC:-}" \ && -f "${WASMLD_HWJS_XMOD_SUSPENDERS:-}" ]]; then export WASMLD_HWJS_XMOD_PARTITION_LIBC="$WASMLD_HWJS_XMOD_SUSPENDERS" echo "wasmcc-glibc: NOTE (-shared): SHARED_PARTITION_CONSUME — consuming libc.so's published partition '$WASMLD_HWJS_XMOD_SUSPENDERS' as the third-.so consumer arm. This also arms the pass's D3 cross-module unwind audit and forces Module2bEnabled on this module." >&2 elif [[ -n "${WASMLD_HWJS_XMOD_PARTITION_LIBC:-}" ]]; then echo "wasmcc-glibc: NOTE (-shared): SHARED_PARTITION_CONSUME — keeping the caller's WASMLD_HWJS_XMOD_PARTITION_LIBC='$WASMLD_HWJS_XMOD_PARTITION_LIBC'." >&2 elif [[ -n "${WASMLD_HWJS_XMOD_SUSPENDERS:-}" ]]; then echo "wasmcc-glibc: WARNING (-shared): SHARED_PARTITION_CONSUME — WASMLD_HWJS_XMOD_SUSPENDERS='$WASMLD_HWJS_XMOD_SUSPENDERS' does not exist; this side module links attribute-keyed (hwjs.abi v1) and its libc imports are seeded NATIVE." >&2 else # Nobody asked at all — and this branch was absent, so such a link went through # in silence. It is not the exotic case: both variables above arrive as # ENVIRONMENT, from whichever build task the caller chose to arm, so every link a # package's own build system runs OUTSIDE that task loses them. ncurses relinks # all eleven of its shared libraries in `make install`, and eleven partition-less # .so files shipped with every libc import seeded NATIVE — libtinfo carrying 89 # coroutine exports where its compile-time link produced 120, and the missing 31 # parking under callers that never bracketed them. That is the third arm of # brintos/linux-in-the-browser#184 (BRINTOS-2685), and a per-recipe opt-in list # cannot close it: a list names recipes, while the defect is a property of which # TASK happens to run the link. # # So resolve it here, where the answer is known without being told: libc.so's # partition ships in this toolchain, beside the libc.so it describes. Correctness # then stops depending on the caller knowing to ask. # # The libc.so PRODUCER cannot reach this: it links through brintld directly # (sysroot/libc-so.mk, LIBC_SO_LINKER_FILES), never through this driver — which # matters because brintld refuses the consumer arm on a producer link. _libc_partition="$TOOLS_DIR/build/libc-glibc.so.partition" if [[ -f "$_libc_partition" ]]; then export WASMLD_HWJS_XMOD_PARTITION_LIBC="$_libc_partition" echo "wasmcc-glibc: NOTE (-shared): SHARED_PARTITION_CONSUME — no partition was requested; defaulting to the one this toolchain ships, '$_libc_partition', as the third-.so consumer arm (BRINTOS-2685)." >&2 else echo "wasmcc-glibc: WARNING (-shared): SHARED_PARTITION_CONSUME — no partition was requested and this toolchain ships none at '$_libc_partition'; this side module links attribute-keyed (hwjs.abi v1) and its libc imports are seeded NATIVE, so a coroutinized libc callee will park under a call this library never bracketed (BRINTOS-2685)." >&2 fi fi unset WASMLD_HWJS_XMOD_SUSPENDERS # BRINTOS-2323-DYNMAIN-CONSUME: the slot the line above just freed is # where the MAIN's partition goes. libc.so's partition says which of libc's # exports park; it says nothing about the main, and until now nothing did — # so a side module dlopen'd by a dynamic main seeded every one of the main's # exports NATIVE by attribute keying, and a coroutinized callee parked under # a caller that never bracketed it. Measured on this image: python3 exports # 1275 coroutines of 1504, crc32c imports 8 of them, and its coroutinized # PyInit_crc32c calls PyModule_Create2 bare — the first-import trap. # # The names-only spelling is the right one HERE and the wrong one above, # which is why this is a second variable rather than a reuse of the first. # wasmld's -shared arm forwards WASMLD_HWJS_XMOD_SUSPENDERS to the pass as # HWJS_CORO_XMOD_SUSPENDERS, the names-only knob, and the pass UNIONS that # suspend surface with the verdict partition set just above # (WASMLD_HWJS_XMOD_PARTITION_LIBC) and refuses any name the two class # differently. Handing the same file to both spellings is what the # SHARED_PARTITION_CONSUME comment refuses, and this keeps them separate. # # An EMPTY partition is skipped rather than forwarded: the pass refuses an # empty list, and the producer installs an honest empty record for a main # that coroutinizes nothing (wasmld WASMLD_DYNMAIN_SUSPENDERS_OUT). if [[ -s "${WASMLD_DYNMAIN_SUSPENDERS:-}" ]]; then export WASMLD_HWJS_XMOD_SUSPENDERS="$WASMLD_DYNMAIN_SUSPENDERS" echo "wasmcc-glibc: NOTE (-shared): DYNMAIN_SUSPENDERS — seeding this side module's import-suspend verdicts from the dynamic main's published partition '$WASMLD_DYNMAIN_SUSPENDERS' ($(grep -c . "$WASMLD_DYNMAIN_SUSPENDERS") coroutine export(s)); the pass unions it with libc.so's partition and refuses any name they class differently (BRINTOS-2323)." >&2 elif [[ -n "${WASMLD_DYNMAIN_SUSPENDERS:-}" && ! -f "${WASMLD_DYNMAIN_SUSPENDERS:-}" ]]; then echo "wasmcc-glibc: WARNING (-shared): DYNMAIN_SUSPENDERS — WASMLD_DYNMAIN_SUSPENDERS='$WASMLD_DYNMAIN_SUSPENDERS' does not exist; this side module seeds the main's exports NATIVE by attribute keying and a coroutinized main export called from it will park unbracketed (BRINTOS-2323)." >&2 elif [[ -n "${WASMLD_DYNMAIN_SUSPENDERS:-}" ]]; then echo "wasmcc-glibc: NOTE (-shared): DYNMAIN_SUSPENDERS — the main's published partition '$WASMLD_DYNMAIN_SUSPENDERS' is EMPTY, so the main coroutinizes no export and there is nothing to seed; linking attribute-keyed (BRINTOS-2323)." >&2 fi WASMLD_DYNAMIC_MAIN=0; export WASMLD_DYNAMIC_MAIN LIBC_IMPORT=0; RT_IMPORT=0 fi # Hand the shared hits to wasmld through the knob it already reads, appended to # whatever the caller set so their entries keep their position — the order the # loader walks. After the -shared/static demotion above, which may have cleared # LIBC_IMPORT: a needed-list on an image that imports no libc.so is one the # loader walks no graph for, and wasmld refuses that combination outright. if [[ ${#needed_sos[@]} -gt 0 && "$LIBC_IMPORT" == "1" ]]; then needed_joined="$(IFS=':'; printf '%s' "${needed_sos[*]}")" if [[ -n "${WASMLD_NEEDED_LIBS:-}" ]]; then export WASMLD_NEEDED_LIBS="${WASMLD_NEEDED_LIBS}:${needed_joined}" else export WASMLD_NEEDED_LIBS="${needed_joined}" fi echo "wasmcc-glibc: NOTE (BRINTOS-884): -l resolved ${#needed_sos[@]} shared librar(y/ies) to needed-graph entries: ${needed_joined}" >&2 fi # The -L directories never reach wasmld (this driver resolves -l itself), and # the needed closure has to look its entries up there too (hwjs-cc#128). if [[ "$LIBC_IMPORT" == "1" && ${#Ldirs[@]} -gt 0 ]]; then export WASMLD_NEEDED_SEARCH_DIRS="$(IFS=':'; printf '%s' "${Ldirs[*]}")" fi # BRINTOS-2689: on a -shared link the SAME hits are this side module's SIBLING # libraries. They are not the main's needed list (the condition above keeps # that true); wasmld records them in the side module's own needed list # (hwjs-cc#128), and the calls into them need a descend bracket, which nothing # told the pass about until this seed. # libc.so's published partition is active on this arm and an active partition is # the whole verdict, so every sibling export fell through NATIVE: the caller # bracketed nothing, the callee parked anyway, and the orphan tripped the next # bracketed call. wasmld derives the names from the modules themselves (it reads # each one's name section) rather than trusting a list assembled here, so this # hands over the paths and nothing else. # # Driver->linker only. A recipe cannot express this: WASMLD_HWJS_XMOD_SUSPENDERS # means libc.so's PUBLISHED partition on a main and a names-only list on a .so, # and one do_compile runs both arms in the same environment — libwebp's does four # -shared links and five main links — so a recipe-level export would silently # replace libc.so's partition on every main in that recipe. # # "Sibling" is the common case and not the whole set: a -lNAME hit resolves against # every -L dir, recipe-sysroot included, so a library from ANOTHER recipe reaches # this list too. That is correct -- the seed is about what this module CALLS, not # about who built it -- but it is a wider surface than the word suggests. # # CONTENT-GATED HERE rather than at resolution: the -lNAME arm above deliberately # skips _hwjs_is_side_module on this arm so an ld script or a host ELF resolves and # is dropped instead of refusing a link that works today (ncurses stages # libtermcap.so as `INPUT(libncurses.so)`). Dropping it from the LINK is not the # same as feeding it to a wasm parser, so the test belongs here. unset WASMLD_SHARED_SIBLING_LIBS if [[ ${#needed_sos[@]} -gt 0 && "$is_shared" == "1" ]]; then sibling_mods=() for so in "${needed_sos[@]}"; do if _hwjs_is_side_module "$so"; then sibling_mods+=( "$so" ) else echo "wasmcc-glibc: NOTE (BRINTOS-2689): -shared link resolved '$so', which is not a wasm side module (an ld script or a host object) — dropped from the link as before, and kept OUT of the sibling suspend seed." >&2 fi done if [[ ${#sibling_mods[@]} -gt 0 ]]; then sibling_joined="$(IFS=':'; printf '%s' "${sibling_mods[*]}")" export WASMLD_SHARED_SIBLING_LIBS="$sibling_joined" echo "wasmcc-glibc: NOTE (BRINTOS-2689): -shared link resolved ${#sibling_mods[@]} sibling librar(y/ies), seeded may-suspend from their own coroutine exports: ${sibling_joined}" >&2 fi fi hwjs_link "$CLANG" "$WASMLD" \ $LINK_TARGET -nostdlib -fno-builtin -fvisibility=default \ "${CORO_FEATURES[@]}" -ffunction-sections -fdata-sections \ "${SHARED_LD[@]}" \ ${CRT_O:+"$CRT_O"} ${RT_O:+"$RT_O"} ${RT_SEED_O:+"$RT_SEED_O"} ${ENTRY_O:+"$ENTRY_O"} "${SYSCALL_O[@]}" \ "${link_args[@]}" \ "${NATIVE_A[@]}" "${SUBSTRATE[@]}" exit $? fi # ---- COMPILE / preprocess -------------------------------------------------- # .S/.s: native (no IR). Otherwise emit bitcode for the coro merge. if [[ "$has_asm_input" == "1" ]]; then exec "$CLANG" $COMPILE_TARGET "${COMMON_INC[@]}" "${CORO_FEATURES[@]}" "$@" fi emit=() compile=0 have_o=0 src="" prev="" for a in "$@"; do case "$a" in -c) compile=1; [[ "${WASMLD_HWJS_CONFTEST:-0}" == "0" ]] && emit=( -emit-llvm ) ;; # BRINTOS-WORKAROUND: brintos/hwjs-cc#39 revisit # The attached form must count too: missing it synthesises a second -o # after the caller's args, so clang writes ./.o and the requested # path never appears — silently, exit 0. waf always emits -o. -o|-o?*) have_o=1 ;; esac if [[ "$prev" != "-o" ]]; then case "$a" in *.c|*.cc|*.cpp|*.cxx|*.C|*.cp|*.c++) src="$a";; esac fi prev="$a" done out=() if [[ "$compile" == "1" && "$have_o" == "0" && -n "$src" ]]; then base="${src##*/}"; base="${base%.*}"; out=( -o "${base}.o" ) fi exec "$CLANG" $COMPILE_TARGET -nostdlibinc -isystem "$GLIBC_SYSINC" -isystem "$GLIBC_UAPI_INC" \ -fno-builtin -fvisibility=default "${CORO_FEATURES[@]}" \ -ffunction-sections -fdata-sections \ "${emit[@]}" "$@" "${out[@]}"