You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
soldr wants reld as its default linker (zackees/soldr#3262). That was tried, shipped, and reverted as premature (zackees/soldr#3263) pending #121. #121 was closed by 8420e6c1 ("escalate silently-ignored flags to lld"). An adversarial re-audit of that commit, of the full native option inventory against ld.lld's, of what real drivers emit, and of input-side semantics shows the prerequisite is not met, and that the current routing model cannot prove it is met. This issue replaces #121's narrow fix with a phased design whose exit criterion is a machine-checked proof:
Every flag, every input format, every architecture, and every input-carried semantic that a supported driver can hand reld is either honored natively, honored by a bundled engine reld routes to, or rejected loudly. Nothing is silently dropped, and nothing is routed to an engine that also drops it.
Until Phase 6's gate is green, soldr keeps SOLDR_LINKER=fast on its current defaults. zackees/soldr#3262 is blocked on this issue.
What the audit found
Everything below was verified against the code on escalate-ignored-flags (8420e6c1), against ld.lld/lld-link/ld64.lld 21.1.8 (rust-lld from the 1.95 toolchain), and against rustc --print link-args / clang -### / gcc -### output on this host.
A. The 8420e6c1 escalation routes to an engine that ignores the same flags
bridge.rs now declares ArchiveGroups, NoStdlib, SortCommon, Stats as capabilities of the lld engine. They are not.
Flag
native reld (before)
ld.lld
Net effect
--start-group / --end-group / -( / -)
no-op (cycles resolved by construction)
"Ignored for compatibility with GNU" (its own --help)
Nothing gained. Link leaves the native engine for no reason.
--sort-common
no-op
accepted silently, no effect
Nothing gained.
--stats
no-op
accepted silently, no stats printed
Nothing gained.
--nostdlib
no-op (native has no built-in search paths)
honored, but lld also has no built-in search paths
Nothing gained.
The capability table was extended by asserting, not by measuring. These four flags are not correctness-affecting on either engine, so the correct classification is satisfied-by-construction, not route. The mis-classification has a real cost: clang and gcc emit --start-group … --end-group on every -static and -static-pie link, so every +crt-static glibc build now silently leaves the native engine.
Side regression: the flags were removed from the native parser entirely, so the documented escape hatch RELD_UNSUPPORTED=ignore --engine=reld with --start-group on the line now fails with "unrecognized option" where it used to link.
The model is missing a class. Today a flag is honored, routed, or unknown. It needs satisfied-by-construction (the native engine's unconditional behavior already implies the flag's semantics), each with a test proving the equivalence.
B. Flags still dropped, mis-routed, or wrongly fatal after 8420e6c1
Flag / input
What happens today
Verified how
-C link-dead-code (rustc omits --gc-sections)
Native defaults gc_sections: true "because it's faster" (args/elf.rs). rustc expresses link-dead-code by not passing --gc-sections, so reld GCs anyway. The user's request is silently violated. GNU ld, lld, and mold all default to no GC.
rustc --print link-args -C link-dead-code has no --gc-sections; code read.
--fix-cortex-a53-835769
In IGNORED_FLAGS: native warns and links. Not in the classifier. ld.lldrejects it. No bundled engine has it, so this must be a loud error.
Classifier lowercases every token, so -X hits the -x arm and is routed to lld as DiscardAll. -X is a native default.
lower == "-x" in collect_requested_capabilities.
-plugin-opt=… + bitcode inputs
rustc -C linker-plugin-lto through clang emits -Wl,-plugin-opt=O0,-plugin-opt=mcpu=x86-64 and no-plugin/-flto. Not a trigger. The link stays native and is handed LLVM bitcode.
rustc emits it on everycdylib, dylib, and proc-macro link, so all of them route to lld. Not a mislink, but the native engine never links a proc-macro, invisibly. mold implements the check natively; it is a version-script lookup.
rustc --print link-args --crate-type proc-macro.
-z lazy
No-op. Native unconditionally emits DF_BIND_NOW + DF_1_NOW. -z now is satisfied by construction; -z lazy is silently converted. Needs a deviation-register entry.
elf_writer.rsdt_flags().
-z nostart-stop-gc, -z notext
No-op handlers. start_stop_eligible retention exists in layout, so nostart-stop-gc is probably satisfied by construction and -z start-stop-gc is the one that would be dropped. Neither has a test.
Unknown -z keyword → warn + drop. Native never errors on text relocations; it just sets DT_TEXTREL when they occur. lld's default is -z text (error). So -z notext is satisfied by construction and the hardening flag -z text is silently lost.
elf_writer.rsDT_TEXTREL; -z fallback arm.
-l:libfoo.a (exact-name library)
rustc emits -l:libfoo.a for -l static:+verbatim=… / #[link(modifiers = "+verbatim")]. The native resolver formats lib{name}.so / lib{name}.a, so the lookup becomes lib:libfoo.a.so and fails "not found". Loud, but wrong; GNU ld and lld honor -l:.
Not declared or not conformant; the mold suite pins each as failing.
z_options and misc groups in mold_skip_tests.toml.
C. ~150 ld.lld options are a hard error natively instead of a route
A mechanical diff of every .long(…) the native ELF parser declares (114) against ld.lld --help (267 long options) leaves roughly 150 options that native reld reports as unrecognized option(s) even though a bundled engine honors them. That is loud, so it is not a silent drop, but it is the opposite of "runs everywhere by routing". Ones real users pass with -C link-arg= or -Wl,:
GNU aliases: -dn/-dy/-call_shared/-non_shared, --library, --library-path, --default-script
GNU-ld-only flags (no bundled engine: --no-warn-rwx-segments, --no-warn-execstack, --gc-keep-exported, --hash-size, --disable-linker-version, -plugin-save-temps) need an explicit disposition too. Most are satisfied by construction (reld never emits those warnings) and should be accepted, not rejected.
D. Architecture and format are host-keyed, not target-keyed
PlatformKind::host() decides ELF/COFF/Mach-O from the host OS unless argv[0] is ld/ld64/reld-link or -flavor is given. Architecture::try_from(e_machine) accepts exactly five machines. Nothing inspects the requested target before choosing an engine.
Direction
What reld receives
What happens
A bundled engine could
clang -m32 / any i386 objects
-m elf_i386 or EM_386 inputs
-m elf_i386 is not yet supported / Unsupported architecture: 0x3
Forwarded to ld64.lld, which rejects driver flags. CI passes only because it also sets -Clinker-flavor=ld64.lld. soldr#3262's "direct reld on macOS" is wrong as written.
reld could unwrap the cc flavor, or soldr injects the flavor flag
E. Input-carried semantics that no flag audit sees
The native engine's behavior on these is decided by section flags and notes in the inputs, not by argv. They must be classified the same way flags are.
x86 ISA-level notes handled; CET flags warn-and-drop
honored
Linker scripts
Top-level OUTPUT_ARCH is silently ignored; any other unknown top-level command (SEARCH_DIR, INSERT, REGION_ALIAS, EXTERN, STARTUP, INCLUDE, NOCROSSREFS) becomes Command::Arg and is later treated as an input path; output-section data commands (LONG, BYTE, FILL, SUBALIGN, NOLOAD, ONLY_IF_R*, >region, :phdr) are unparsed. extern "C++" in version scripts bails.
full grammar
Relocatable output -r
supported, with a documented TODO that debug sections are not cleaned up like ld/mold
honored
Default --gc-sections without the flag
GCs (see §B -C link-dead-code)
no GC
F. The bridged formats have their own silent-ignore class that reld cannot see
lld-link accepts, without any diagnostic, link.exe flags it does not implement: /PROFILE, /LTCG, /LTCG:INCREMENTAL, /INCREMENTAL, /ALLOWBIND, /FASTFAIL, /GENPROFILE, /USEPROFILE, /EMITPOGOPHASEINFO, /LTCGOUT, /ASSEMBLYMODULE among others (all probed). An unknown /FLAG is treated as an input file (could not open '/BOGUSFLAG'). ld64.lld likewise silently accepts -no_deduplicate, -object_path_lto, -no_adhoc_codesign, -search_paths_first, -objc_abi_version, -export_dynamic, -encryptable, -reproducible, … and rejects -warn_duplicate_libraries and -ld_classic. Because the bridge forwards argv verbatim and "never falls back to link.exe/ld64" (decision B2), these are silent drops with no more-capable engine. They need a deviation register sourced from lld's own Options.td "ignored" groups, not from probing, since silence does not distinguish honored from ignored.
G. Observability gaps that block soldr
reld --version on macOS/Windows is bridged to lld (ci.yml says so). soldr's linker_candidate_identity keys its PEP 517 fallback cache on --version, so a reld upgrade never invalidates it there.
Routing is visible only with RELD_LOG_ENGINE=1. Nothing lets a build system require a route, so a regression like §A is invisible in CI.
Bridge discovery spawns rustc --print sysroot and rustc -vV on every routed link, and picks the rust-lld of whatever toolchain the current directory'srust-toolchain.toml selects. So the lld version that runs varies by cwd, and the invocation log records the engine name but not the resolved linker path or version.
Native reld honors cargo's jobserver (jobserver::Client in args.rs); a bridged lld does not, so every routed link under cargo -j N oversubscribes the machine. The bridge should translate acquired tokens into --threads=N.
Three response-file grammars coexist: args.rs::read_args_from_file (quotes + escapes, used by the native parser and the classifier), bridge.rs::response_arguments (quotes only, used for the audit log's output path), and lld's --rsp-quoting. The mold suite already pins response-file-quoting.sh as failing.
--version prints Reld <v> (compatible with GNU linkers). CMake ≥ 3.29 linker-type detection and similar tooling match GNU ld / LLD / mold; reld is detected as none of them, so linker-specific flag sets may be skipped. Decide which family string to claim.
Two native code paths are unreachable under default routing and absent from the engine table: the GNU-plugin LTO path (linker_plugins.rs, acceptance tests gated on RequiresLinkerPlugin) because -plugin is classified Lto → lld first, and the native Mach-O backend (macho.rs, macho_writer.rs, Args::MachO → macho::link_for_arch) because default_for(MachO) is always the bridge. The table lies in both directions.
I. Declared is not conformant: the mold skip list is an inventory the flag audit cannot see
A flag can be declared, parsed, and stored, and still not do what GNU ld/lld/mold do. The Phase 1 external-suite ratchet (crates/reld/tests/external_tests/mold_skip_tests.toml, #14) already pins that debt, 17 groups deep, and none of it is connected to routing today. A presence-based table would mark every one of these Native and pass.
symbol_versioning, tls, icf_semantics, as_needed_gc, static_dso, symtab_binding: semantic gaps not attached to any flag: --as-needed interacting with GC, TLS LE/common cases, symbol-table binding, version-script edge cases.
misc (~40 tests, "awaiting classification") includes declared flags that fail: -z nocopyreloc, -z initfirst, --strip-*, --dynamic-linker, --noinhibit-exec, --no-undefined-version, -Bno-symbolic, --defsym with a missing symbol, --discard-*, common-symbol handling, weak-undef cases, symtab*, textrel2.
ignore group deviations that are real user-visible differences, not message-format noise: gc-sections.sh, start-lib.sh, undefined.sh, undefined2.sh, whole-archive.sh all "pass when --no-gc-sections is passed" (the §B GC-default deviation, pinned five times); hash-style.sh (none unsupported); as-needed-weak.sh; copy relocations placed in .bss instead of .copyrel; plt-symbols.sh; build-id data size.
lto: the native plugin path "doesn't support LTO without an explicit plugin", -m llvm is unsupported, COMDAT in LTO is unsupported. That is the same unreachable path as §G.
Every skip entry that names a flag or -z keyword must resolve to a table disposition: Requires(lld) until fixed natively, or Native with the mold test un-skipped. Semantic groups without a flag gate the soldr default through the #14 ratchet count instead.
J. Route-dependent divergence: the same argv means different things on different engines
Routing is only honest if a link produces the same semantics whichever engine runs it. Today it does not.
The Nix RUNPATH derivation (feat(elf): derive RUNPATH for Nix-store -L directories #111) is native-only.add_nix_rpath_entries runs inside the native ELF parser, reading NIX_STORE / NIX_DONT_SET_RPATH. A link that routes to lld for any reason gets no derived RUNPATH. On NixOS with a rustup toolchain that is every proc-macro and cdylib link today (§B --no-undefined-version), every +crt-static link (§A), and every LTO link: the exact rust-lld default on x86_64-unknown-linux-gnu produces binaries without RUNPATH on NixOS rust-lang/rust#162781 failure reld exists to fix comes back on the routed path. crates/reld/tests/nix_rpath.rs only ever links natively, so nothing catches it.
29 reld-only or GNU-only options are forwarded verbatim to lld.comm of reld's declared long options against ld.lld's inventory: --debug-fuel, --discard-sframe, --fallocate-output-file/--no-…, --fork/--no-fork, --gc-stats-ignore, --got-plt-syms, --madvise-huge-pages/--no-…, --nix-rpath, --no-identity-comment, --no-string-merge, --no-threads, --no-update-in-place/--update-in-place, --prepopulate-maps, --reld-experimental-sframe, --reld-experiments, --rpath-link, --sym-info, --thread-count, --time, --verbose-gc-stats, --write-gc-stats. Only --validate-output, --write-layout, --write-trace are classified NativeControl, and only --engine= is stripped. Any of the other 26 on a line that also routes (say --time --icf=all) is handed to lld, which rejects it as an unknown argument. The reld-only performance knobs (--fork, --madvise-huge-pages, --prepopulate-maps, --fallocate-output-file) are exactly what a tuned soldr profile would pass.
Capability sets are copy-pasted across formats.COFF_LLD_ENGINE and MACHO_LLD_ENGINE declare the ELF-shaped LLD_CAPABILITIES (Icf, CortexA53Erratum, VersionScriptPolicy, …). The /LTCG arm in the classifier lives on the ELF-only path and can never fire for a COFF link. Harmless today only because non-ELF requirement lists are always empty.
A bridged child killed by a signal exits 1.status.code().unwrap_or(1) discards the signal, so cargo reports "exit code: 1" with nothing attached. This is the same bare-exit-1 shape soldr#1992 spent a session on.
K. Native defaults that differ from GNU ld with no flag on the line
These never show up in a flag audit because nothing is passed. Each needs a deviation-register entry and a decision.
Default
GNU ld
ld.lld
native reld
Effect
GC
off
off
on (gc_sections: true)
§B -C link-dead-code; five mold tests pass only with --no-gc-sections
-rpath tag
DT_RPATH (enable_new_dtags off)
DT_RUNPATH
DT_RUNPATH (enable_new_dtags: true)
LD_LIBRARY_PATH precedence and transitive-dependency search differ from a GNU-ld build with the same -Wl,-rpath
Executable stack
inferred from .note.GNU-stack (missing note → execstack + warning)
off
off, no inference (execstack: false)
arch-x86_64-execstack-if-needed.sh is skipped
--hash-style
sysv (distro-configured both)
sysv/both
both
fine
RELRO
on
on
on
fine
-z text / notext
notext
text
notext, cannot be turned on (§B)
hardening flag lost
Text relocations warning
warns
error
silent
warn-textrel tests skipped
Copy relocations of read-only data
.bss.rel.ro (RELRO-protected)
.bss.rel.ro
.bss
writable after RELRO; copyrel-relro*.sh skipped
SHF_GNU_RETAIN under --gc-sections
kept
kept
dropped
§L: coverage and #[used] registries
LD_RUN_PATH
default rpath
ignored
ignored
matches lld; differs from GNU ld
L. Retained sections, coverage, and environment: GC drops what the compiler said to keep
rustc marks #[used] statics and coverage sections SHF_GNU_RETAIN. Verified with readelf on rustc 1.95 objects: #[used] #[link_section = "myreg"] → myreg AR; -C instrument-coverage → __llvm_covmap R, __llvm_covfun GR, __llvm_prf_names AR. reld-core has zero references to SHF_GNU_RETAIN, to __llvm_cov*, or to __llvm_prf_*, and rustc always passes --gc-sections. So the native engine GCs the coverage mapping (cargo llvm-cov gets empty or partial reports) and any #[used] static in a custom section that nothing references (registry patterns: inventory, linkme on sections without __start_ references, plugin tables). lld keeps all of them. No reld test exercises -C instrument-coverage.
LD_RUN_PATH is honored by GNU ld as the default -rpath and by neither lld, mold, nor reld (the only env vars native reld reads are NIX_DONT_SET_RPATH, NIX_STORE, PATH, RELD_PRINT_ALLOCATIONS, RELD_TEST_IGNORE_FORMAT). A deviation-register entry, not a bug, but autotools-era builds rely on it.
Non-UTF-8 argv panics.Args::new(std::env::args) uses the panicking accessor; the classifier's arg.to_str() silently skips the same tokens. A Latin-1 path on the line is a crash in one place and invisible in the other. lld handles bytes.
Bridge discovery goes through the rustup proxy.rustc --print sysroot under a rust-toolchain.toml that names an uninstalled toolchain makes rustup download a toolchain from inside a linker invocation (rustup's default auto-install). Discovery must honor RUSTUP_TOOLCHAIN/an explicit RELD_BRIDGE_LINKER, or resolve rust-lld relative to the rustc that spawned reld (cargo exports RUSTC/CARGO), never by re-resolving the cwd.
Copy relocations land in .bss instead of .bss.rel.ro (five copyrel-*.sh mold tests skipped as "we put copy relocation info to .bss"). Copy-relocated const data from a shared library is therefore writable after RELRO, a hardening regression GNU ld, lld, and mold do not have.
H. What is fine (so nobody re-audits it)
clang --ld-path=<abs reld> beats rustc's own injected -B gcc-ld -fuse-ld=lld, in either order, no warning. soldr's Linux injection is sound.
Every token rustc emits on ordinary Linux links across opt-level, strip=*, relocation-model=static, panic=abort, prefer-dynamic, lto=fat, target-cpu, split-debuginfo, link-self-contained=no is declared natively: --as-needed, -Bstatic/-Bdynamic, --eh-frame-hdr, --gc-sections, -O1, --strip-all, --strip-debug, -z noexecstack, -z relro, -z now, -pie. Only --no-undefined-version (§B) and +crt-static (§A) leave the native engine.
clang -fsanitize=address tokens (--whole-archive, --dynamic-list=, --no-as-needed) and -shared -rdynamic -Bsymbolic-functions --build-id are declared natively.
COFF/Mach-O response files are left opaque to the router; correct, since MSVC emits UTF-16.
--engine= is stripped before forwarding and validated against format and capabilities.
Every field the ELF parser stores is consumed somewhere (checked mechanically; the accessor names differ from the field names, e.g. z_stack_size → stack_size_override). No declared flag is parsed into a dead field.
Design
D1. Four-way classification over the union of engine inventories
Replace SILENTLY_IGNORED_FLAGS, IGNORED_FLAGS, DEFAULT_FLAGS, DEFAULT_SHORT_FLAGS, the -z fallback, every warn_unsupported value arm, and the if lower == … chain in collect_requested_capabilities with one declarative table in bridge.rs:
enumDisposition{/// Native parser implements it. Names the test that proves it.Native,/// Native engine's unconditional behavior already implies it. Names the/// equivalence test (native vs lld, on the flag's probe workload, comparing/// the property the flag governs).SatisfiedByConstruction,/// Native cannot honor it; route to the first engine whose *measured*/// capability set includes it.Requires(Capability),/// No bundled engine honors it. Loud error unless RELD_UNSUPPORTED=ignore.Unsupported,}structFlagRule{/// Exact spellings, case-sensitive for GNU/ld64, case-insensitive for COFF.spellings:&'static[&'staticstr],value:ValueMatch,// `--flag=value`, `-z value`, `/FLAG:value`disposition:Disposition,emitters:&'static[Emitter],// rustc, clang, gcc, cc-rs, cmake, hand-written}
Rules:
The table's key set is the union of lld's Options.td inventories for ELF, MinGW, COFF, and Mach-O at the pinned LLVM version (vendored, regenerated by a script), plus GNU ld's option list, plus reld-only flags. A CI check fails if any inventory entry lacks a rule. This is what turns §C's ~150 "unrecognized option" errors into routes, and what gives §F a deviation register.
Every Capability an engine claims carries provenance: Measured { probe } (Phase 2 runs the probe in CI) or Documented { source } pointing at the engine's option table. Options lld marks "ignored for compatibility" are SatisfiedByConstruction on that engine, never a capability.
Native handlers may not be |_, _| Ok(()). A no-op is only legal as a SatisfiedByConstruction rule with its test.
Native is a conformance claim, not a parse claim. Each Native rule names its conformance evidence: an acceptance test, or the mold-suite tests that exercise it. A rule whose named mold tests are in the skip list cannot be Native; it is Requires(lld) until the skip is removed (§I).
Library names, not just flags: -l:exact, -l with --push-state/-Bstatic, --start-lib/--end-lib, and +verbatim all get rules with tests.
reld --print-flag-table emits the table; docs/flags.md is generated from it and checked in CI. README and polylinker.md "shipped vs designed" claims are derived from it.
D2. Route by target, architecture, and inputs, not host
select_route gains a TargetProbe that runs before format dispatch, in this order:
-flavor / argv[0] (existing).
-m <emulation> / OUTPUT_FORMAT / -EB/-EL: the five native ELF targets → native; every other ELF emulation lld knows → ld.lld; i386pep, i386pe, arm64pe, thumb2pe → new Engine::MingwLld (ld.lld -m …, lld's MinGW driver). This is the engine chore: raise MSRV to Rust 1.95 and define Windows GNU acceptance #90 is missing.
-arch, -platform_version, -syslibroot, -dead_strip → Mach-O via ld64.lld.
Inputs. Before parsing, peek the first object/archive member: EI_CLASS, EI_DATA, e_machine outside the native set → ld.lld; LLVM bitcode magic (BC\xC0\xDE or the wrapper header) → Requires(Lto). Routing on inputs is what makes -C linker-plugin-lto safe regardless of which flags the driver emitted.
Linker scripts: if -T/--script/an implicit script uses any command outside the native grammar (INSERT, REGION_ALIAS, LONG, FILL, >region, :phdr, extern "C++" in version scripts, …) → Requires(FullLinkerScript) → ld.lld. Never Command::Arg an unknown command.
Otherwise host default.
D3. Deviation register
Every SatisfiedByConstruction rule, every -z no-op, the --gc-sections default, -z lazy → now, the -r debug-section TODO, and every lld/lld-link/ld64.lld "ignored for compatibility" option gets an entry with: what the user asked, what reld does, and the acceptance test that pins the property (archive-cycle result, DT_FLAGS bits, section retention for SHF_GNU_RETAIN and __start_/__stop_, .deplibs expansion, TEXTREL presence, search-path set). The register is user-visible in docs/flags.md.
--gc-sections default changes to match every other linker: GC only when asked, --no-gc-sections respected, and -C link-dead-code gets a corpus row proving dead code survives.
D4. Emitter corpus: prove coverage against what drivers actually send
ci/flag_corpus.py captures linker argv from real emitters on every CI host and asserts that every token (and every input's magic, and every linker-script command) matches exactly one FlagRule. An unmatched token fails CI. This is the "no gaps" proof soldr is waiting for.
Emitter
Configurations
rustc --print link-args
bin, cdylib, dylib, proc-macro, staticlib; opt-level 0/3; -C lto=off/thin/fat; -C linker-plugin-lto; +crt-static; -C relocation-model=static; panic=abort; -C strip=debuginfo/symbols; -C link-dead-code; -C prefer-dynamic; -C link-self-contained=+linker on/off; -C linker-flavor=ld64.lld and default on Apple
regular file, existing running executable (ETXTBSY), -o /dev/null, tmpfs, overlayfs (Docker), 9p/virtiofs (VM shares), a path with spaces via @response
cc-rs / cmake
existing ci/consumer_acceptance.py C/C++ projects, plus one CMake project using --version-script, --whole-archive, -Bsymbolic-functions, -Map, and __attribute__((retain))
Windows / macOS
rustc's full MSVC set (/DEBUG, /PDBALTPATH, /OPT:REF,NOICF, /NXCOMPAT, /defaultlib:), cc-rs on MSVC, rustc's ld64 set, cc-rs on macOS; every token classified against the vendored COFF/Mach-O inventories
The corpus is versioned (ci/flag-corpus.lock.json, same pattern as clang-link-corpus.lock.json) so a toolchain bump that introduces a new flag fails loudly rather than drifting.
D5. Observability and identity
RELD_REQUIRE_ENGINE=<name>: fail the link if routing selects anything else. CI pins "rustc default bin link is native", "proc-macro link is native", "+crt-static is native", "-C linker-plugin-lto is lld".
reld --reld-identity: prints reld's version, git SHA, and flag-table hash and never bridges. soldr's identity probe switches to it.
RELD_INVOCATION_LOG records gain a per-token disposition list so an audit can show why a link left native.
Bridge discovery is cached per process tree (env-keyed), resolves relative to RUSTC/RUSTUP_TOOLCHAIN when present, never triggers a rustup install, and records the resolved linker path + version in RELD_INVOCATION_LOG; discovery no longer depends on cwd.
One response-file grammar, conformant with response-file-quoting.sh, used by the classifier, the native parser, and the audit log.
--version claims one linker family string deliberately (test against CMake's detection regexes).
RELD_STRICT_BRIDGE=1: for bridged formats, promote any token whose deviation-register entry is "ignored by lld-link/ld64.lld" to an error. Default stays permissive for compatibility; soldr's CI turns it on.
D6. Route invariance
Anything reld adds on top of "be a linker" is an argv-level transformation, applied before engine selection, so every engine sees the same request:
The Nix RUNPATH derivation becomes derive_nix_rpath(argv) -> Vec<"-rpath", dir> appended to the forwarded argv for every route, with nix_rpath.rs parameterized over RELD_ENGINE=reld|lld.
Every reld-only option is either NativeControl (forces native; conflicts loudly with a routed requirement) or Stripped (a performance/diagnostic knob that is dropped, with a RELD_LOG_ENGINE note, when the link leaves native). The table decides; nothing reld-only reaches a child linker.
Capability sets are declared per format; the classifier has one arm set per grammar (GNU, COFF, ld64) and a COFF/ld64 arm can never live on the ELF path.
A bridged child's signal is propagated (exit(128+sig) and a stderr line naming the signal and the engine).
Phases
Each phase is independently mergeable with its own tests. Phases 0–3 are on soldr's critical path; 4–5 are required for "runs everywhere"; 6 is the gate.
Case-sensitive classifier for GNU flags. Test: -X native, -x routes.
-plugin-opt becomes an Lto trigger (interim until D2 step 5).
--fix-cortex-a53-835769 → Unsupported (loud).
--gc-sections default → off unless requested. Corpus row for -C link-dead-code. Un-skip gc-sections.sh, start-lib.sh, undefined.sh, undefined2.sh, whole-archive.sh.
-z text → Requires(lld) until implemented natively (Phase 2); -z notext → SatisfiedByConstruction with a TEXTREL test.
-l:name exact-name resolution implemented natively (it is a path formatting fix). Corpus row for -l static:+verbatim=.
All 26 unclassified reld-only options become NativeControl or Stripped (D6). Test: --time --icf=all links through lld with --time dropped and logged; --sym-info=x --icf=all errors loudly.
Nix RUNPATH derivation applied on the lld route; nix_rpath.rs runs under both RELD_ENGINE values.
Bridged child signals propagated.
Exit: +crt-static and -static C links route native; -C linker-plugin-lto routes to lld; -C link-dead-code keeps dead code; all asserted with RELD_LOG_ENGINE.
Phase 1 — The flag table (D1)
Vendor lld's ELF/MinGW/COFF/Mach-O Options.td inventories and GNU ld's option list at pinned versions; generator script + CI drift check.
Introduce FlagRule/Disposition; migrate every existing arm, table, -z sub-option, and value fallback. Every inventory entry gets a rule or CI fails. §C's ~150 options become Requires(LldOption) routes; GNU-only warning suppressors become SatisfiedByConstruction.
Capability provenance (Measured/Documented).
reld --print-flag-table; docs/flags.md generated and checked.
Exit: SILENTLY_IGNORED_FLAGS, IGNORED_FLAGS, DEFAULT_FLAGS, DEFAULT_SHORT_FLAGS no longer exist; no Ok(()) no-op handler exists outside the table; RELD_UNSUPPORTED=ignore --engine=reld works again for satisfied-by-construction flags.
Phase 2 — Native implementations of the cheap, common escalations
Everything rustc emits on ordinary links must stay native or the default is a perf regression in disguise.
SHF_GNU_RETAIN honored in GC; .deplibs expanded; -z start-stop-gc/nostart-stop-gc both real with tests. Acceptance test: -C instrument-coverage binary produces a .profraw that llvm-cov can map, and a #[used] #[link_section] static with no references survives.
Copy relocations of read-only data go to .bss.rel.ro; un-skip copyrel-relro*.sh.
args_os everywhere; non-UTF-8 paths link.
-z keywords lld honors and drivers emit (text, separate-code/noseparate-code, nognustack, global, dynamic-undefined-weak, force-bti, pac-plt, bti-report, cet-report, shstk, ibt): each Native, SatisfiedByConstruction with a D3 test, or Requires(ZKeyword(kw)). None remains warn-and-continue.
-z text native (error on text relocations, matching lld's default when the flag is given); text-relocation warning by default.
§K decisions: .note.GNU-stack inference for the executable-stack default (match GNU ld and lld's warning), and an explicit, documented choice for DT_RPATH vs DT_RUNPATH when neither --enable-new-dtags nor --disable-new-dtags is given.
Burn down §I for every flag rustc, clang, gcc, and cc-rs emit: unsupported_options, z_options, and misc entries that name such a flag become Native with the mold test un-skipped, or Requires(lld). The remaining semantic groups (tls, as_needed_gc, symbol_versioning, symtab_binding, static_dso) get a ratchet count that must be non-increasing and is reported in the Phase 6 gate.
Decide the two unreachable native paths (§G): GNU-plugin LTO and native Mach-O either enter the engine table as Capability/Engine entries with probes, or are deleted. No unreachable engine.
Exit: every D4 rustc row except -C lto=*/linker-plugin-lto runs under RELD_REQUIRE_ENGINE=reld.
Phase 3 — Target-keyed routing (D2), Linux and macOS hosts
TargetProbe from -m, -EB/-EL, OUTPUT_FORMAT, -arch/-platform_version, /OUT:, input e_machine/EI_CLASS/EI_DATA, bitcode magic, and linker-script grammar.
Engine::MingwLld. clang --target=x86_64-w64-mingw32 --ld-path=reld links on Linux.
i386, arm32, mips, s390x, ppc64be, riscv32 objects route to ld.lld instead of Unsupported architecture.
Mach-O cross from Linux via ld64.lld (rustc -Clinker-flavor=ld64.lld and clang --target=arm64-apple-darwin).
macOS host rule, decided here: either reld accepts rustc's darwin-cc flavor by unwrapping -Wl, and translating driver-only flags (-arch, -nodefaultlibs, -mmacosx-version-min → -platform_version), or soldr injects -Clinker-flavor=ld64.lld. The soldr issue is updated with the choice.
Phase 4 — Windows host parity and bridged-format deviation register
Windows host + windows-gnu argv → MingwLld, not lld-link.
Vendor lld-link's and ld64.lld's "ignored for compatibility" groups into the deviation register (§F); RELD_STRICT_BRIDGE promotes them to errors.
Unknown /FLAG on COFF is diagnosed as an unknown flag, not could not open '/FLAG'.
Audit rustc's and cc-rs's full MSVC and ld64 token sets against the vendored inventories.
Exit: D4 rows for both Windows targets and both Apple targets pass on their runners with RELD_STRICT_BRIDGE=1.
Phase 5 — Observability and identity (D5)
RELD_REQUIRE_ENGINE, --reld-identity, per-token disposition in RELD_INVOCATION_LOG, RELD_STRICT_BRIDGE.
ci/consumer_acceptance.py runs the Linux Rust project under RELD_REQUIRE_ENGINE=reld and its LTO variant under RELD_REQUIRE_ENGINE=lld.
Exit: soldr's identity probe can switch to --reld-identity on every host.
Phase 6 — The no-gaps gate (D4) and soldr hand-off
ci/flag_corpus.py in CI on all three hosts with the full matrix; any unmatched token, input magic, or script command is red.
README and polylinker.md regenerated from the flag table.
Only when this job is green on main: reopen Make reld the default linker for soldr soldr#3262 with the corrected platform rules (Linux: clang --ld-path=<abs> plus a clang-on-PATH probe so a missing driver falls back instead of breaking every build; macOS: whichever Phase 3 chose; Windows: direct reld; identity via --reld-identity), plus three soldr-side fixes the audit surfaced:
Cross targets.TargetKind::Linux injects clang --ld-path for any -linux- triple with no --target=<triple> or sysroot, so soldr cargo build --target aarch64-unknown-linux-gnu from x86_64 hands host-targeted clang aarch64 objects. Inject -C link-arg=--target=<triple> or restrict the reld default to host-native triples. The musl row must actually link, not just print args.
Silent fallback cache. The PEP 517 path retries a failed reld link with the platform linker and caches that fallback keyed by the (currently broken on macOS/Windows, §G) identity. A reld bug is then hidden for the cache lifetime. The fallback must be attached to the failing compile's diagnostics and reported, never cached silently (soldr#1992's diagnostic gap, third instance).
soldr#1992 root cause. rust-lld as a direct -C linker fails proc-macro DLL links on MSVC with a bare exit 1. reld's Windows bridge is that same lld-link, so "extend the strip list to reld" (soldr#3262's plan) papers over a reld-visible failure. Phase 4 root-causes it before reld is exempted from proc-macro links.
Testing criteria
Every claim in this issue is backed by a test at one of five layers. A phase is done when its rows are green in CI on the hosts named; a row that cannot run on a host is reported as unavailable, never counted as passing (same rule as the #63 benchmark gate).
Layer
Where
What it proves
Pass rule
Unit (routing)
crates/reld-core/src/bridge.rs tests
For a given argv (+ response files, + env), the selected engine, reason, forwarded argv, and stripped tokens are exactly as the flag table says
One test per FlagRule, generated from the table; a rule without a test does not compile (#[test] name derived from the spelling)
Equivalence (satisfied-by-construction)
crates/reld/tests/acceptance.rs, one sources/ fixture per rule
Native output has the property the flag governs, equal to ld.lld's with the flag: archive-cycle resolution, DT_FLAGS, retained sections, .deplibs expansion, TEXTREL, RPATH tag
Property comparison via reld-diff, not byte identity; the test names the rule, the rule names the test
Conformance (native semantics)
mold external suite (external_test_suites/mold, mold_skip_tests.toml) and reld-difftest --reference-linker bfd
Declared-Native flags behave like GNU ld/mold
No skip entry may name a flag whose disposition is Native; the semantic-group skip count is published per run and must not increase (ratchet)
Corpus (coverage of real emitters)
new ci/flag_corpus.py + ci/flag-corpus.lock.json, all three hosts
Every token, input magic, and linker-script command any listed emitter produces matches exactly one rule; the selected route for each row is the expected one
Unmatched token → red; unexpected route (checked with RELD_REQUIRE_ENGINE) → red; lock drift on toolchain bump → red until re-approved
Real Rust, C, C++, CMake projects build, run, and are self-deterministic on each host through the expected engine; the invocation log names the engine, resolved linker path, and version
Existing gates plus: RELD_REQUIRE_ENGINE per project, RELD_STRICT_BRIDGE=1 on Windows/macOS, nix_rpath.rs under both RELD_ENGINE values
Specific criteria that must exist by name, so they can be checked off:
Routing and classification
flag_table_covers_lld_inventory: every vendored ld.lld/MinGW/lld-link/ld64.lld option and every GNU ld option has a rule (Phase 1)
no_capability_is_ignored_for_compat: no Requires(cap) targets an engine whose option table marks the flag ignored (Phase 1)
target_probe_picks_engine_before_format: -m i386pep, -m elf_i386, -arch arm64, /OUT:, EM_ARM input, big-endian input, LTO bitcode, script with INSERT each select the documented engine on a Linux host (Phase 3) — probe half done in feat(core): add TargetProbe to derive link target from argv and inputs (#178) #182 (TargetProbe, not yet wired into engine selection)
require_engine_fails_on_mismatch: RELD_REQUIRE_ENGINE=reld + -flto is a loud error naming both engines (Phase 5)
Semantics that were silently wrong
link_dead_code_keeps_dead_code: rustc -C link-dead-code binary contains an unreferenced function (Phase 0)
z_text_errors_on_textrel, z_notext_allows_and_flags: with a PIC-less object (Phase 2)
windows_gnu_from_linux_links_and_runs (cross-ship, Phase 3) and windows_gnu_on_windows_routes_to_mingw_lld (Phase 4)
macho_from_linux_links: ld64.lld route produces a Mach-O the macOS runner executes (Phase 3)
macos_rustc_default_flavor_links: whichever Phase 3 chose, CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=reld with no extra rustflags builds the sqlite e2e (Phase 3)
strict_bridge_rejects_ignored_msvc_flags: /PROFILE under RELD_STRICT_BRIDGE=1 errors; without it, it is logged (Phase 4)
msvc_proc_macro_dll_links: the soldr#1992 shape (--crate-type proc-macro -C prefer-dynamic, 4 parallel) passes 20/20 through the bridge, or the root cause is documented and gated (Phase 4)
Bridge operation
discovery_never_installs_toolchains: with rust-toolchain.toml naming an uninstalled channel and RUSTUP_AUTO_INSTALL=0-equivalent assertions, a routed link resolves via RUSTC/RELD_BRIDGE_LINKER or fails loudly; never spawns a download (Phase 5)
jobserver_tokens_become_threads: under MAKEFLAGS with 2 tokens the bridged lld receives --threads=2 (Phase 5)
response_file_quoting: the mold response-file-quoting.sh case passes natively and the same file yields the same tokens in the classifier (Phase 5) — tokenizer unified in feat(core): single GNU response-file tokenizer and Phase 0 classifier tests (#179) #183; the mold case now parses natively and only differs in error wording, so it sits in the ignore group
reld_identity_never_bridges: on all three hosts --reld-identity prints reld's version with no child process (Phase 5)
Gate
flag_corpus job green on Linux, Windows, macOS with the full D4 matrix and lock file (Phase 6)
semantic-group ratchet count published and non-increasing over the last 10 main runs (Phase 6)
docs/flags.md, README, polylinker.md regenerated from the table with no manual edits (Phase 6)
Acceptance for this issue
No Ok(()) no-op flag handler exists outside the flag table; every table entry has a disposition with a test or probe reference; every entry in the vendored lld and GNU ld inventories has a rule.
Every capability an engine claims was measured by a probe in CI; lld's "ignored for compatibility" options are deviation-register entries, not capabilities.
Every token, input magic, and linker-script command in the emitter corpus matches exactly one rule, on all three CI hosts.
rustc's ordinary bin, cdylib, proc-macro, +crt-static, and -C link-dead-code links run natively under RELD_REQUIRE_ENGINE=reld with correct semantics; -C lto, -C linker-plugin-lto, and bitcode inputs route to lld under RELD_REQUIRE_ENGINE=lld.
SHF_GNU_RETAIN and .deplibs are honored natively with tests. -z text and -l:name work.
No mold skip-list entry names a flag whose table disposition is Native; the semantic-group ratchet count is published by the Phase 6 job.
Bridged links record the resolved lld path and version, honor the jobserver, and share one response-file grammar with the native parser.
Route invariance: nix_rpath.rs passes under RELD_ENGINE=lld; no reld-only option reaches a child linker; a signal-killed child is reported as a signal.
Every §K default has a deviation-register entry and, where it changed, an acceptance test.
i386/arm32/mips/s390x objects, windows-gnu, Mach-O, and COFF links from a Linux host route to a bundled lld engine rather than failing in the ELF parser.
Summary
soldr wants reld as its default linker (zackees/soldr#3262). That was tried, shipped, and reverted as premature (zackees/soldr#3263) pending #121. #121 was closed by
8420e6c1("escalate silently-ignored flags to lld"). An adversarial re-audit of that commit, of the full native option inventory against ld.lld's, of what real drivers emit, and of input-side semantics shows the prerequisite is not met, and that the current routing model cannot prove it is met. This issue replaces #121's narrow fix with a phased design whose exit criterion is a machine-checked proof:Until Phase 6's gate is green, soldr keeps
SOLDR_LINKER=faston its current defaults. zackees/soldr#3262 is blocked on this issue.What the audit found
Everything below was verified against the code on
escalate-ignored-flags(8420e6c1), againstld.lld/lld-link/ld64.lld21.1.8 (rust-lldfrom the 1.95 toolchain), and againstrustc --print link-args/clang -###/gcc -###output on this host.A. The
8420e6c1escalation routes to an engine that ignores the same flagsbridge.rsnow declaresArchiveGroups,NoStdlib,SortCommon,Statsas capabilities of thelldengine. They are not.ld.lld--start-group/--end-group/-(/-)--help)--sort-common--stats--nostdlibThe capability table was extended by asserting, not by measuring. These four flags are not correctness-affecting on either engine, so the correct classification is satisfied-by-construction, not route. The mis-classification has a real cost:
clangandgccemit--start-group … --end-groupon every-staticand-static-pielink, so every+crt-staticglibc build now silently leaves the native engine.Side regression: the flags were removed from the native parser entirely, so the documented escape hatch
RELD_UNSUPPORTED=ignore --engine=reldwith--start-groupon the line now fails with "unrecognized option" where it used to link.The model is missing a class. Today a flag is honored, routed, or unknown. It needs satisfied-by-construction (the native engine's unconditional behavior already implies the flag's semantics), each with a test proving the equivalence.
B. Flags still dropped, mis-routed, or wrongly fatal after
8420e6c1-C link-dead-code(rustc omits--gc-sections)gc_sections: true"because it's faster" (args/elf.rs). rustc expresses link-dead-code by not passing--gc-sections, so reld GCs anyway. The user's request is silently violated. GNU ld, lld, and mold all default to no GC.rustc --print link-args -C link-dead-codehas no--gc-sections; code read.--fix-cortex-a53-835769IGNORED_FLAGS: native warns and links. Not in the classifier.ld.lldrejects it. No bundled engine has it, so this must be a loud error.ld.lld --fix-cortex-a53-835769 …→unknown argument.-X(--discard-locals)-Xhits the-xarm and is routed to lld asDiscardAll.-Xis a native default.lower == "-x"incollect_requested_capabilities.-plugin-opt=…+ bitcode inputsrustc -C linker-plugin-ltothrough clang emits-Wl,-plugin-opt=O0,-plugin-opt=mcpu=x86-64and no-plugin/-flto. Not a trigger. The link stays native and is handed LLVM bitcode.rustc --print link-args -C linker-plugin-lto -C linker=clang.--no-undefined-versioncdylib,dylib, andproc-macrolink, so all of them route to lld. Not a mislink, but the native engine never links a proc-macro, invisibly. mold implements the check natively; it is a version-script lookup.rustc --print link-args --crate-type proc-macro.-z lazyDF_BIND_NOW+DF_1_NOW.-z nowis satisfied by construction;-z lazyis silently converted. Needs a deviation-register entry.elf_writer.rsdt_flags().-z nostart-stop-gc,-z notextstart_stop_eligibleretention exists in layout, sonostart-stop-gcis probably satisfied by construction and-z start-stop-gcis the one that would be dropped. Neither has a test.-z <kw>warn_unsupported→ warning + continue. Drivers emit:-z text(clang-static-pie),-z separate-code,-z start-stop-gc,-z force-bti,-z pac-plt,-z bti-report=,-z cet-report=,-z shstk,-z ibt,-z dynamic-undefined-weak,-z nognustack,-z global. lld honors all of these.clang -### -static-pie;-zfallback arm.--pack-dyn-relocs=android*,--icf=<other>,--sort-section=<other>-z text-zkeyword → warn + drop. Native never errors on text relocations; it just setsDT_TEXTRELwhen they occur. lld's default is-z text(error). So-z notextis satisfied by construction and the hardening flag-z textis silently lost.elf_writer.rsDT_TEXTREL;-zfallback arm.-l:libfoo.a(exact-name library)-l:libfoo.afor-l static:+verbatim=…/#[link(modifiers = "+verbatim")]. The native resolver formatslib{name}.so/lib{name}.a, so the lookup becomeslib:libfoo.a.soand fails "not found". Loud, but wrong; GNU ld and lld honor-l:.rustc --print link-args -l static:+verbatim=libfoo.a;input_data.rs.--hash-style=none,--compress-debug-sections=zlib:<level>/zstd:<level>hash-style.sh,compress-debug-sections-*-level.shin the mold skip list.-z nodefaultlib,-z nodump,-z rodynamic,-z sectionheader,-z start-stop-visibility=,-z initfirst,-z cet-report=,-z dynamic-undefined-weakz_optionsandmiscgroups inmold_skip_tests.toml.C. ~150 ld.lld options are a hard error natively instead of a route
A mechanical diff of every
.long(…)the native ELF parser declares (114) againstld.lld --help(267 long options) leaves roughly 150 options that native reld reports asunrecognized option(s)even though a bundled engine honors them. That is loud, so it is not a silent drop, but it is the opposite of "runs everywhere by routing". Ones real users pass with-C link-arg=or-Wl,:--emit-relocs,--image-base,--oformat/-b binary/--format,--omagic,--just-symbols,--filter,--init/--fini,--undefined-glob,--keep-unique,--unique,--package-metadata,--dependent-libraries/--no-dependent-libraries--symbol-ordering-file,--call-graph-ordering-file,--call-graph-profile-sort,--bp-*,--shuffle-sections,--randomize-section-padding-Map/--print-map,--cref,--print-gc-sections,--print-icf-sections,--why-extract,--why-live,--trace-symbol,--verbose,--reproduce,--time-trace,--error-limit,--no-warnings/-w,--warn-backrefs,--warn-common,--check-sections--lto-*,--thinlto-*,--fat-lto-objects,--save-temps,--mllvm,--load-pass-plugin,--opt-remarks-*--android-memtag-*,--execute-only,--fix-cortex-a8,--be8,--cmse-implib/--in-implib/--out-implib,--power10-stubs,--toc-optimize,--pcrel-optimize,--relax-gp,--target1-*,--target2-dn/-dy/-call_shared/-non_shared,--library,--library-path,--default-scriptGNU-ld-only flags (no bundled engine:
--no-warn-rwx-segments,--no-warn-execstack,--gc-keep-exported,--hash-size,--disable-linker-version,-plugin-save-temps) need an explicit disposition too. Most are satisfied by construction (reld never emits those warnings) and should be accepted, not rejected.D. Architecture and format are host-keyed, not target-keyed
PlatformKind::host()decides ELF/COFF/Mach-O from the host OS unless argv[0] isld/ld64/reld-linkor-flavoris given.Architecture::try_from(e_machine)accepts exactly five machines. Nothing inspects the requested target before choosing an engine.clang -m32/ any i386 objects-m elf_i386orEM_386inputs-m elf_i386 is not yet supported/Unsupported architecture: 0x3ld.lldlinks i386-m armelf_linux_eabi,elf32lriscv,-EB, …ld.lldlinks all of themx86_64-pc-windows-gnu(mingw driver)-m i386pep -lmoldname …-m i386pep is not yet supported. #90 tracks windows-gnu with no engine behind it.ld.lld -m i386pep(lld's MinGW driver)windows-gnu(gcc driver invokingreld.exe)lld-link, which does not speak GNU syntaxaarch64-apple-darwin(clang --target=arm64-apple-darwin --ld-path=reld)-arch arm64 -platform_version …ld64.lldships in every Rust toolchainx86_64-pc-windows-msvc/OUT: /DEBUG …/OUT:…as an input file. The classifier already knows/LTCG.lld-linkdarwin-cc),CARGO_TARGET_*_LINKER=reld-Wl,-dead_strip -nodefaultlibs -arch … -mmacosx-version-min=…ld64.lld, which rejects driver flags. CI passes only because it also sets-Clinker-flavor=ld64.lld. soldr#3262's "directreldon macOS" is wrong as written.E. Input-carried semantics that no flag audit sees
The native engine's behavior on these is decided by section flags and notes in the inputs, not by argv. They must be classified the same way flags are.
SHF_GNU_RETAIN(clang__attribute__((retain)), rustc#[used(linker)])reld-core. GC can drop sections the compiler marked retained.SHT_LLVM_DEPENDENT_LIBRARIES(.deplibs, clang#pragma comment(lib)on ELF).note.gnu.propertymerging,-z force-bti,-z pac-plt-zforms warn-and-drop-z cet-report,shstk,ibt)OUTPUT_ARCHis silently ignored; any other unknown top-level command (SEARCH_DIR,INSERT,REGION_ALIAS,EXTERN,STARTUP,INCLUDE,NOCROSSREFS) becomesCommand::Argand is later treated as an input path; output-section data commands (LONG,BYTE,FILL,SUBALIGN,NOLOAD,ONLY_IF_R*,>region,:phdr) are unparsed.extern "C++"in version scripts bails.-r--gc-sectionswithout the flag-C link-dead-code)F. The bridged formats have their own silent-ignore class that reld cannot see
lld-linkaccepts, without any diagnostic,link.exeflags it does not implement:/PROFILE,/LTCG,/LTCG:INCREMENTAL,/INCREMENTAL,/ALLOWBIND,/FASTFAIL,/GENPROFILE,/USEPROFILE,/EMITPOGOPHASEINFO,/LTCGOUT,/ASSEMBLYMODULEamong others (all probed). An unknown/FLAGis treated as an input file (could not open '/BOGUSFLAG').ld64.lldlikewise silently accepts-no_deduplicate,-object_path_lto,-no_adhoc_codesign,-search_paths_first,-objc_abi_version,-export_dynamic,-encryptable,-reproducible, … and rejects-warn_duplicate_librariesand-ld_classic. Because the bridge forwards argv verbatim and "never falls back tolink.exe/ld64" (decision B2), these are silent drops with no more-capable engine. They need a deviation register sourced from lld's ownOptions.td"ignored" groups, not from probing, since silence does not distinguish honored from ignored.G. Observability gaps that block soldr
reld --versionon macOS/Windows is bridged to lld (ci.ymlsays so). soldr'slinker_candidate_identitykeys its PEP 517 fallback cache on--version, so a reld upgrade never invalidates it there.RELD_LOG_ENGINE=1. Nothing lets a build system require a route, so a regression like §A is invisible in CI.rustc --print sysrootandrustc -vVon every routed link, and picks therust-lldof whatever toolchain the current directory'srust-toolchain.tomlselects. So the lld version that runs varies by cwd, and the invocation log records the engine name but not the resolved linker path or version.jobserver::Clientinargs.rs); a bridged lld does not, so every routed link undercargo -j Noversubscribes the machine. The bridge should translate acquired tokens into--threads=N.args.rs::read_args_from_file(quotes + escapes, used by the native parser and the classifier),bridge.rs::response_arguments(quotes only, used for the audit log's output path), and lld's--rsp-quoting. The mold suite already pinsresponse-file-quoting.shas failing.--versionprintsReld <v> (compatible with GNU linkers). CMake ≥ 3.29 linker-type detection and similar tooling matchGNU ld/LLD/mold; reld is detected as none of them, so linker-specific flag sets may be skipped. Decide which family string to claim.linker_plugins.rs, acceptance tests gated onRequiresLinkerPlugin) because-pluginis classifiedLto → lldfirst, and the native Mach-O backend (macho.rs,macho_writer.rs,Args::MachO → macho::link_for_arch) becausedefault_for(MachO)is always the bridge. The table lies in both directions.I. Declared is not conformant: the mold skip list is an inventory the flag audit cannot see
A flag can be declared, parsed, and stored, and still not do what GNU ld/lld/mold do. The Phase 1 external-suite ratchet (
crates/reld/tests/external_tests/mold_skip_tests.toml, #14) already pins that debt, 17 groups deep, and none of it is connected to routing today. A presence-based table would mark every one of theseNativeand pass.unsupported_options(47 tests):--compress-debug-sections=zlib:N,--defsym=foo=_GLOBAL_OFFSET_TABLE_,--dynamic-listdata symbols,--emit-relocs,--execute-only,--filter,--image-base,--init/--fini,--library,--oformat=binary,--omagic,--no-warnings,--package-metadata,--require-defined,--retain-symbols-file,--section-alignment,--trace-symbol,--undefined-glob,--warn-common,--warn-once,--audit/--depaudit,--default-symver,--spare-program-headers,-Map,--repro, response-file quoting,--fatal-warnings+-warn-common, thin/static archive cases that pass with GNU ld.z_options:cet-report,dynamic-undefined-weak,nodefaultlib,nodump,rodynamic,sectionheader,start-stop-visibility.symbol_versioning,tls,icf_semantics,as_needed_gc,static_dso,symtab_binding: semantic gaps not attached to any flag:--as-neededinteracting with GC, TLS LE/common cases, symbol-table binding, version-script edge cases.misc(~40 tests, "awaiting classification") includes declared flags that fail:-z nocopyreloc,-z initfirst,--strip-*,--dynamic-linker,--noinhibit-exec,--no-undefined-version,-Bno-symbolic,--defsymwith a missing symbol,--discard-*, common-symbol handling, weak-undef cases,symtab*,textrel2.ignoregroup deviations that are real user-visible differences, not message-format noise:gc-sections.sh,start-lib.sh,undefined.sh,undefined2.sh,whole-archive.shall "pass when--no-gc-sectionsis passed" (the §B GC-default deviation, pinned five times);hash-style.sh(noneunsupported);as-needed-weak.sh; copy relocations placed in.bssinstead of.copyrel;plt-symbols.sh; build-id data size.lto: the native plugin path "doesn't support LTO without an explicit plugin",-m llvmis unsupported, COMDAT in LTO is unsupported. That is the same unreachable path as §G.Every skip entry that names a flag or
-zkeyword must resolve to a table disposition:Requires(lld)until fixed natively, orNativewith the mold test un-skipped. Semantic groups without a flag gate the soldr default through the #14 ratchet count instead.J. Route-dependent divergence: the same argv means different things on different engines
Routing is only honest if a link produces the same semantics whichever engine runs it. Today it does not.
add_nix_rpath_entriesruns inside the native ELF parser, readingNIX_STORE/NIX_DONT_SET_RPATH. A link that routes to lld for any reason gets no derived RUNPATH. On NixOS with a rustup toolchain that is every proc-macro and cdylib link today (§B--no-undefined-version), every+crt-staticlink (§A), and every LTO link: the exact rust-lld default on x86_64-unknown-linux-gnu produces binaries without RUNPATH on NixOS rust-lang/rust#162781 failure reld exists to fix comes back on the routed path.crates/reld/tests/nix_rpath.rsonly ever links natively, so nothing catches it.commof reld's declared long options against ld.lld's inventory:--debug-fuel,--discard-sframe,--fallocate-output-file/--no-…,--fork/--no-fork,--gc-stats-ignore,--got-plt-syms,--madvise-huge-pages/--no-…,--nix-rpath,--no-identity-comment,--no-string-merge,--no-threads,--no-update-in-place/--update-in-place,--prepopulate-maps,--reld-experimental-sframe,--reld-experiments,--rpath-link,--sym-info,--thread-count,--time,--verbose-gc-stats,--write-gc-stats. Only--validate-output,--write-layout,--write-traceare classifiedNativeControl, and only--engine=is stripped. Any of the other 26 on a line that also routes (say--time --icf=all) is handed to lld, which rejects it as an unknown argument. The reld-only performance knobs (--fork,--madvise-huge-pages,--prepopulate-maps,--fallocate-output-file) are exactly what a tuned soldr profile would pass.COFF_LLD_ENGINEandMACHO_LLD_ENGINEdeclare the ELF-shapedLLD_CAPABILITIES(Icf,CortexA53Erratum,VersionScriptPolicy, …). The/LTCGarm in the classifier lives on the ELF-only path and can never fire for a COFF link. Harmless today only because non-ELF requirement lists are always empty.status.code().unwrap_or(1)discards the signal, so cargo reports "exit code: 1" with nothing attached. This is the same bare-exit-1 shape soldr#1992 spent a session on.K. Native defaults that differ from GNU ld with no flag on the line
These never show up in a flag audit because nothing is passed. Each needs a deviation-register entry and a decision.
gc_sections: true)-C link-dead-code; five mold tests pass only with--no-gc-sections-rpathtagDT_RPATH(enable_new_dtagsoff)DT_RUNPATHDT_RUNPATH(enable_new_dtags: true)LD_LIBRARY_PATHprecedence and transitive-dependency search differ from a GNU-ld build with the same-Wl,-rpath.note.GNU-stack(missing note → execstack + warning)execstack: false)arch-x86_64-execstack-if-needed.shis skipped--hash-styleboth)both-z text/notextwarn-textreltests skipped.bss.rel.ro(RELRO-protected).bss.rel.ro.bsscopyrel-relro*.shskippedSHF_GNU_RETAINunder--gc-sections#[used]registriesLD_RUN_PATHL. Retained sections, coverage, and environment: GC drops what the compiler said to keep
#[used]statics and coverage sectionsSHF_GNU_RETAIN. Verified withreadelfon rustc 1.95 objects:#[used] #[link_section = "myreg"]→myreg AR;-C instrument-coverage→__llvm_covmap R,__llvm_covfun GR,__llvm_prf_names AR.reld-corehas zero references toSHF_GNU_RETAIN, to__llvm_cov*, or to__llvm_prf_*, and rustc always passes--gc-sections. So the native engine GCs the coverage mapping (cargo llvm-covgets empty or partial reports) and any#[used]static in a custom section that nothing references (registry patterns:inventory,linkmeon sections without__start_references, plugin tables). lld keeps all of them. No reld test exercises-C instrument-coverage.LD_RUN_PATHis honored by GNU ld as the default-rpathand by neither lld, mold, nor reld (the only env vars native reld reads areNIX_DONT_SET_RPATH,NIX_STORE,PATH,RELD_PRINT_ALLOCATIONS,RELD_TEST_IGNORE_FORMAT). A deviation-register entry, not a bug, but autotools-era builds rely on it.Args::new(std::env::args)uses the panicking accessor; the classifier'sarg.to_str()silently skips the same tokens. A Latin-1 path on the line is a crash in one place and invisible in the other. lld handles bytes.rustc --print sysrootunder arust-toolchain.tomlthat names an uninstalled toolchain makes rustup download a toolchain from inside a linker invocation (rustup's defaultauto-install). Discovery must honorRUSTUP_TOOLCHAIN/an explicitRELD_BRIDGE_LINKER, or resolverust-lldrelative to therustcthat spawned reld (cargo exportsRUSTC/CARGO), never by re-resolving the cwd..bssinstead of.bss.rel.ro(fivecopyrel-*.shmold tests skipped as "we put copy relocation info to.bss"). Copy-relocatedconstdata from a shared library is therefore writable after RELRO, a hardening regression GNU ld, lld, and mold do not have.H. What is fine (so nobody re-audits it)
clang --ld-path=<abs reld>beats rustc's own injected-B gcc-ld -fuse-ld=lld, in either order, no warning. soldr's Linux injection is sound.opt-level,strip=*,relocation-model=static,panic=abort,prefer-dynamic,lto=fat,target-cpu,split-debuginfo,link-self-contained=nois declared natively:--as-needed,-Bstatic/-Bdynamic,--eh-frame-hdr,--gc-sections,-O1,--strip-all,--strip-debug,-z noexecstack,-z relro,-z now,-pie. Only--no-undefined-version(§B) and+crt-static(§A) leave the native engine.-fsanitize=addresstokens (--whole-archive,--dynamic-list=,--no-as-needed) and-shared -rdynamic -Bsymbolic-functions --build-idare declared natively.--engine=is stripped before forwarding and validated against format and capabilities.z_stack_size→stack_size_override). No declared flag is parsed into a dead field.Design
D1. Four-way classification over the union of engine inventories
Replace
SILENTLY_IGNORED_FLAGS,IGNORED_FLAGS,DEFAULT_FLAGS,DEFAULT_SHORT_FLAGS, the-zfallback, everywarn_unsupportedvalue arm, and theif lower == …chain incollect_requested_capabilitieswith one declarative table inbridge.rs:Rules:
Options.tdinventories for ELF, MinGW, COFF, and Mach-O at the pinned LLVM version (vendored, regenerated by a script), plus GNU ld's option list, plus reld-only flags. A CI check fails if any inventory entry lacks a rule. This is what turns §C's ~150 "unrecognized option" errors into routes, and what gives §F a deviation register.Capabilityan engine claims carries provenance:Measured { probe }(Phase 2 runs the probe in CI) orDocumented { source }pointing at the engine's option table. Options lld marks "ignored for compatibility" areSatisfiedByConstructionon that engine, never a capability.|_, _| Ok(()). A no-op is only legal as aSatisfiedByConstructionrule with its test.Nativeis a conformance claim, not a parse claim. EachNativerule names its conformance evidence: an acceptance test, or the mold-suite tests that exercise it. A rule whose named mold tests are in the skip list cannot beNative; it isRequires(lld)until the skip is removed (§I).-l:exact,-lwith--push-state/-Bstatic,--start-lib/--end-lib, and+verbatimall get rules with tests.reld --print-flag-tableemits the table;docs/flags.mdis generated from it and checked in CI. README andpolylinker.md"shipped vs designed" claims are derived from it.D2. Route by target, architecture, and inputs, not host
select_routegains aTargetProbethat runs before format dispatch, in this order:-flavor/ argv[0] (existing).-m <emulation>/OUTPUT_FORMAT/-EB/-EL: the five native ELF targets → native; every other ELF emulation lld knows →ld.lld;i386pep,i386pe,arm64pe,thumb2pe→ newEngine::MingwLld(ld.lld -m …, lld's MinGW driver). This is the engine chore: raise MSRV to Rust 1.95 and define Windows GNU acceptance #90 is missing.-arch,-platform_version,-syslibroot,-dead_strip→ Mach-O viald64.lld./OUT:,/DEFAULTLIB:,/SUBSYSTEM:(COFF grammar, case-insensitive) →lld-link.EI_CLASS,EI_DATA,e_machineoutside the native set →ld.lld; LLVM bitcode magic (BC\xC0\xDEor the wrapper header) →Requires(Lto). Routing on inputs is what makes-C linker-plugin-ltosafe regardless of which flags the driver emitted.-T/--script/an implicit script uses any command outside the native grammar (INSERT,REGION_ALIAS,LONG,FILL,>region,:phdr,extern "C++"in version scripts, …) →Requires(FullLinkerScript)→ld.lld. NeverCommand::Argan unknown command.D3. Deviation register
Every
SatisfiedByConstructionrule, every-zno-op, the--gc-sectionsdefault,-z lazy → now, the-rdebug-section TODO, and every lld/lld-link/ld64.lld "ignored for compatibility" option gets an entry with: what the user asked, what reld does, and the acceptance test that pins the property (archive-cycle result,DT_FLAGSbits, section retention forSHF_GNU_RETAINand__start_/__stop_,.deplibsexpansion, TEXTREL presence, search-path set). The register is user-visible indocs/flags.md.--gc-sectionsdefault changes to match every other linker: GC only when asked,--no-gc-sectionsrespected, and-C link-dead-codegets a corpus row proving dead code survives.D4. Emitter corpus: prove coverage against what drivers actually send
ci/flag_corpus.pycaptures linker argv from real emitters on every CI host and asserts that every token (and every input's magic, and every linker-script command) matches exactly oneFlagRule. An unmatched token fails CI. This is the "no gaps" proof soldr is waiting for.rustc --print link-args-C lto=off/thin/fat;-C linker-plugin-lto;+crt-static;-C relocation-model=static;panic=abort;-C strip=debuginfo/symbols;-C link-dead-code;-C prefer-dynamic;-C link-self-contained=+linkeron/off;-C linker-flavor=ld64.lldand default on Applex86_64-unknown-linux-gnu,-musl,aarch64-unknown-linux-gnu,i686-unknown-linux-gnu,x86_64-pc-windows-msvc,x86_64-pc-windows-gnu,aarch64-apple-darwin,x86_64-apple-darwin,thumbv7em-none-eabi(cortex-m-rtlink.x/memory.x),x86_64-unknown-none(custom-Tscript)clang -###/gcc -###-static,-static-pie,-shared,-m32,-flto,-fsanitize=address,-rdynamic,-Wl,-Bsymbolic-functions,--ld-pathwith rustc's-fuse-ld=lldstill present, Debian/Fedora/NixOS wrapper hardening sets (-z pack-relative-relocs,--package-metadata,-z now,--as-needed,--no-warn-rwx-segments)-o /dev/null, tmpfs, overlayfs (Docker), 9p/virtiofs (VM shares), a path with spaces via@responsecc-rs/cmakeci/consumer_acceptance.pyC/C++ projects, plus one CMake project using--version-script,--whole-archive,-Bsymbolic-functions,-Map, and__attribute__((retain))/DEBUG,/PDBALTPATH,/OPT:REF,NOICF,/NXCOMPAT,/defaultlib:), cc-rs on MSVC, rustc's ld64 set, cc-rs on macOS; every token classified against the vendored COFF/Mach-O inventoriesThe corpus is versioned (
ci/flag-corpus.lock.json, same pattern asclang-link-corpus.lock.json) so a toolchain bump that introduces a new flag fails loudly rather than drifting.D5. Observability and identity
RELD_REQUIRE_ENGINE=<name>: fail the link if routing selects anything else. CI pins "rustc default bin link is native", "proc-macro link is native", "+crt-staticis native", "-C linker-plugin-ltois lld".reld --reld-identity: prints reld's version, git SHA, and flag-table hash and never bridges. soldr's identity probe switches to it.RELD_INVOCATION_LOGrecords gain a per-tokendispositionlist so an audit can show why a link left native.RUSTC/RUSTUP_TOOLCHAINwhen present, never triggers a rustup install, and records the resolved linker path + version inRELD_INVOCATION_LOG; discovery no longer depends on cwd.--threads=<jobserver tokens>so routed links respectcargo -j.response-file-quoting.sh, used by the classifier, the native parser, and the audit log.--versionclaims one linker family string deliberately (test against CMake's detection regexes).RELD_STRICT_BRIDGE=1: for bridged formats, promote any token whose deviation-register entry is "ignored by lld-link/ld64.lld" to an error. Default stays permissive for compatibility; soldr's CI turns it on.D6. Route invariance
Anything reld adds on top of "be a linker" is an argv-level transformation, applied before engine selection, so every engine sees the same request:
derive_nix_rpath(argv) -> Vec<"-rpath", dir>appended to the forwarded argv for every route, withnix_rpath.rsparameterized overRELD_ENGINE=reld|lld.NativeControl(forces native; conflicts loudly with a routed requirement) orStripped(a performance/diagnostic knob that is dropped, with aRELD_LOG_ENGINEnote, when the link leaves native). The table decides; nothing reld-only reaches a child linker.exit(128+sig)and a stderr line naming the signal and the engine).Phases
Each phase is independently mergeable with its own tests. Phases 0–3 are on soldr's critical path; 4–5 are required for "runs everywhere"; 6 is the gate.
Phase 0 — Stop the bleeding
ArchiveGroups,NoStdlib,SortCommon,StatsasSatisfiedByConstruction; remove them fromLLD_CAPABILITIES; restore the flags to the native parser. Keep the Escalate silently-ignored flags to lld — never drop linker semantics (reld-as-default prerequisite) #121 tests but invert them: these flags stay native.-Xnative,-xroutes.-plugin-optbecomes anLtotrigger (interim until D2 step 5).--fix-cortex-a53-835769→Unsupported(loud).--gc-sectionsdefault → off unless requested. Corpus row for-C link-dead-code. Un-skipgc-sections.sh,start-lib.sh,undefined.sh,undefined2.sh,whole-archive.sh.-z text→Requires(lld)until implemented natively (Phase 2);-z notext→SatisfiedByConstructionwith a TEXTREL test.-l:nameexact-name resolution implemented natively (it is a path formatting fix). Corpus row for-l static:+verbatim=.NativeControlorStripped(D6). Test:--time --icf=alllinks through lld with--timedropped and logged;--sym-info=x --icf=allerrors loudly.nix_rpath.rsruns under bothRELD_ENGINEvalues.+crt-staticand-staticC links route native;-C linker-plugin-ltoroutes to lld;-C link-dead-codekeeps dead code; all asserted withRELD_LOG_ENGINE.Phase 1 — The flag table (D1)
Options.tdinventories and GNU ld's option list at pinned versions; generator script + CI drift check.FlagRule/Disposition; migrate every existing arm, table,-zsub-option, and value fallback. Every inventory entry gets a rule or CI fails. §C's ~150 options becomeRequires(LldOption)routes; GNU-only warning suppressors becomeSatisfiedByConstruction.Measured/Documented).reld --print-flag-table;docs/flags.mdgenerated and checked.SILENTLY_IGNORED_FLAGS,IGNORED_FLAGS,DEFAULT_FLAGS,DEFAULT_SHORT_FLAGSno longer exist; noOk(())no-op handler exists outside the table;RELD_UNSUPPORTED=ignore --engine=reldworks again for satisfied-by-construction flags.Phase 2 — Native implementations of the cheap, common escalations
Everything rustc emits on ordinary links must stay native or the default is a perf regression in disguise.
--no-undefined-version/--undefined-version: native.--fatal-warnings/--no-fatal-warnings,--color-diagnostics[=…],--no-warnings/-w,--error-limit: native (they are diagnostics plumbing).SHF_GNU_RETAINhonored in GC;.deplibsexpanded;-z start-stop-gc/nostart-stop-gcboth real with tests. Acceptance test:-C instrument-coveragebinary produces a.profrawthatllvm-covcan map, and a#[used] #[link_section]static with no references survives..bss.rel.ro; un-skipcopyrel-relro*.sh.args_oseverywhere; non-UTF-8 paths link.-zkeywords lld honors and drivers emit (text,separate-code/noseparate-code,nognustack,global,dynamic-undefined-weak,force-bti,pac-plt,bti-report,cet-report,shstk,ibt): eachNative,SatisfiedByConstructionwith a D3 test, orRequires(ZKeyword(kw)). None remains warn-and-continue.-z textnative (error on text relocations, matching lld's default when the flag is given); text-relocation warning by default..note.GNU-stackinference for the executable-stack default (match GNU ld and lld's warning), and an explicit, documented choice forDT_RPATHvsDT_RUNPATHwhen neither--enable-new-dtagsnor--disable-new-dtagsis given.unsupported_options,z_options, andmiscentries that name such a flag becomeNativewith the mold test un-skipped, orRequires(lld). The remaining semantic groups (tls,as_needed_gc,symbol_versioning,symtab_binding,static_dso) get a ratchet count that must be non-increasing and is reported in the Phase 6 gate.Capability/Engineentries with probes, or are deleted. No unreachable engine.rustcrow except-C lto=*/linker-plugin-ltoruns underRELD_REQUIRE_ENGINE=reld.Phase 3 — Target-keyed routing (D2), Linux and macOS hosts
TargetProbefrom-m,-EB/-EL,OUTPUT_FORMAT,-arch/-platform_version,/OUT:, inpute_machine/EI_CLASS/EI_DATA, bitcode magic, and linker-script grammar.Engine::MingwLld.clang --target=x86_64-w64-mingw32 --ld-path=reldlinks on Linux.ld.lldinstead ofUnsupported architecture.ld64.lld(rustc-Clinker-flavor=ld64.lldand clang--target=arm64-apple-darwin).darwin-ccflavor by unwrapping-Wl,and translating driver-only flags (-arch,-nodefaultlibs,-mmacosx-version-min→-platform_version), or soldr injects-Clinker-flavor=ld64.lld. The soldr issue is updated with the choice.thumbv7em/x86_64-unknown-nonecorpus rows route to lld with their scripts intact.Phase 4 — Windows host parity and bridged-format deviation register
MingwLld, notlld-link.RELD_STRICT_BRIDGEpromotes them to errors./FLAGon COFF is diagnosed as an unknown flag, notcould not open '/FLAG'.RELD_STRICT_BRIDGE=1.Phase 5 — Observability and identity (D5)
RELD_REQUIRE_ENGINE,--reld-identity, per-tokendispositioninRELD_INVOCATION_LOG,RELD_STRICT_BRIDGE.ci/consumer_acceptance.pyruns the Linux Rust project underRELD_REQUIRE_ENGINE=reldand its LTO variant underRELD_REQUIRE_ENGINE=lld.--reld-identityon every host.Phase 6 — The no-gaps gate (D4) and soldr hand-off
ci/flag_corpus.pyin CI on all three hosts with the full matrix; any unmatched token, input magic, or script command is red.polylinker.mdregenerated from the flag table.main: reopen Make reld the default linker for soldr soldr#3262 with the corrected platform rules (Linux:clang --ld-path=<abs>plus aclang-on-PATH probe so a missing driver falls back instead of breaking every build; macOS: whichever Phase 3 chose; Windows: directreld; identity via--reld-identity), plus three soldr-side fixes the audit surfaced:TargetKind::Linuxinjectsclang --ld-pathfor any-linux-triple with no--target=<triple>or sysroot, sosoldr cargo build --target aarch64-unknown-linux-gnufrom x86_64 hands host-targeted clang aarch64 objects. Inject-C link-arg=--target=<triple>or restrict the reld default to host-native triples. The musl row must actually link, not just print args.-C linkerfails proc-macro DLL links on MSVC with a bare exit 1. reld's Windows bridge is that same lld-link, so "extend the strip list to reld" (soldr#3262's plan) papers over a reld-visible failure. Phase 4 root-causes it before reld is exempted from proc-macro links.Testing criteria
Every claim in this issue is backed by a test at one of five layers. A phase is done when its rows are green in CI on the hosts named; a row that cannot run on a host is reported as unavailable, never counted as passing (same rule as the #63 benchmark gate).
crates/reld-core/src/bridge.rstestsFlagRule, generated from the table; a rule without a test does not compile (#[test]name derived from the spelling)crates/reld/tests/acceptance.rs, onesources/fixture per ruleld.lld's with the flag: archive-cycle resolution,DT_FLAGS, retained sections,.deplibsexpansion, TEXTREL, RPATH tagreld-diff, not byte identity; the test names the rule, the rule names the testexternal_test_suites/mold,mold_skip_tests.toml) andreld-difftest --reference-linker bfdNativeflags behave like GNU ld/moldNative; the semantic-group skip count is published per run and must not increase (ratchet)ci/flag_corpus.py+ci/flag-corpus.lock.json, all three hostsRELD_REQUIRE_ENGINE) → red; lock drift on toolchain bump → red until re-approvedci/consumer_acceptance.py,ci/linker_modes.py,cross-ship.yml,linker-artifacts.ymlRELD_REQUIRE_ENGINEper project,RELD_STRICT_BRIDGE=1on Windows/macOS,nix_rpath.rsunder bothRELD_ENGINEvaluesSpecific criteria that must exist by name, so they can be checked off:
Routing and classification
flag_table_covers_lld_inventory: every vendored ld.lld/MinGW/lld-link/ld64.lld option and every GNU ld option has a rule (Phase 1)no_capability_is_ignored_for_compat: noRequires(cap)targets an engine whose option table marks the flag ignored (Phase 1)classifier_is_case_sensitive:-Xnative,-xroutes;--ICF=allis unknown (Phase 0) — done in feat(core): single GNU response-file tokenizer and Phase 0 classifier tests (#179) #183static_group_flags_stay_native:--start-group … --end-groupfromclang -static→ native (Phase 0) — done in feat(core): single GNU response-file tokenizer and Phase 0 classifier tests (#179) #183plugin_opt_and_bitcode_route_to_lld:-plugin-opt=alone, and bitcode magic alone, each route (Phase 0 / 3) — done in feat(core): single GNU response-file tokenizer and Phase 0 classifier tests (#179) #183reld_only_flags_never_reach_child: for each of the 26 reld-only options, with a routed flag on the line, the forwarded argv excludes it and the log names it (Phase 0) — done in feat(core): single GNU response-file tokenizer and Phase 0 classifier tests (#179) #183target_probe_picks_engine_before_format:-m i386pep,-m elf_i386,-arch arm64,/OUT:, EM_ARM input, big-endian input, LTO bitcode, script withINSERTeach select the documented engine on a Linux host (Phase 3) — probe half done in feat(core): add TargetProbe to derive link target from argv and inputs (#178) #182 (TargetProbe, not yet wired into engine selection)require_engine_fails_on_mismatch:RELD_REQUIRE_ENGINE=reld+-fltois a loud error naming both engines (Phase 5)Semantics that were silently wrong
link_dead_code_keeps_dead_code: rustc-C link-dead-codebinary contains an unreferenced function (Phase 0)z_text_errors_on_textrel,z_notext_allows_and_flags: with a PIC-less object (Phase 2)exact_name_library_resolves:-l:libfoo.a(Phase 0)gnu_retain_survives_gc:#[used] #[link_section]static with no references is present after--gc-sections(Phase 2) — done in test(acceptance): pin SHF_GNU_RETAIN survives --gc-sections (reld#123 Phase 2) #147 (retain-sectionfixture)instrument_coverage_roundtrip:-C instrument-coveragebinary →.profraw→llvm-cov reportshows the function (Phase 2)deplibs_are_linked: object with.deplibsnaminglibmlinks without-lm(Phase 2) — done in feat(core): honor .deplibs (SHT_LLVM_DEPENDENT_LIBRARIES) natively (#180) #181 (deplibsfixture)copyrel_readonly_is_relro: copy-relocatedconstfrom a DSO is insidePT_GNU_RELRO(Phase 2) — done in fix(elf): keep copy relocations of read-only symbols inside RELRO #155 (copy-relocation-relrofixture)no_undefined_version_native: rustc proc-macro link is native and a version script naming an undefined symbol errors (Phase 2)nix_runpath_on_every_route:nix_rpath.rsparameterized overRELD_ENGINE=reld|lld(Phase 0) — done in test: pin the bridged-signal path and RUNPATH on both engines #156execstack_inferred_from_missing_note,rpath_tag_default_documented(Phase 2) —rpath_tag_default_documenteddone in test(acceptance): activate NoDynamic so the RPATH tag is actually pinned (reld#123) #164;execstack_inferred_from_missing_notestill openFormats and hosts
windows_gnu_from_linux_links_and_runs(cross-ship, Phase 3) andwindows_gnu_on_windows_routes_to_mingw_lld(Phase 4)macho_from_linux_links:ld64.lldroute produces a Mach-O the macOS runner executes (Phase 3)macos_rustc_default_flavor_links: whichever Phase 3 chose,CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=reldwith no extra rustflags builds the sqlite e2e (Phase 3)strict_bridge_rejects_ignored_msvc_flags:/PROFILEunderRELD_STRICT_BRIDGE=1errors; without it, it is logged (Phase 4)msvc_proc_macro_dll_links: the soldr#1992 shape (--crate-type proc-macro -C prefer-dynamic, 4 parallel) passes 20/20 through the bridge, or the root cause is documented and gated (Phase 4)Bridge operation
discovery_never_installs_toolchains: withrust-toolchain.tomlnaming an uninstalled channel andRUSTUP_AUTO_INSTALL=0-equivalent assertions, a routed link resolves viaRUSTC/RELD_BRIDGE_LINKERor fails loudly; never spawns a download (Phase 5)bridged_child_signal_is_reported: a child killed by SIGSEGV yields exit 139 and a stderr line naming the engine (Phase 0) — done in test: pin the bridged-signal path and RUNPATH on both engines #156jobserver_tokens_become_threads: underMAKEFLAGSwith 2 tokens the bridged lld receives--threads=2(Phase 5)response_file_quoting: the moldresponse-file-quoting.shcase passes natively and the same file yields the same tokens in the classifier (Phase 5) — tokenizer unified in feat(core): single GNU response-file tokenizer and Phase 0 classifier tests (#179) #183; the mold case now parses natively and only differs in error wording, so it sits in theignoregroupreld_identity_never_bridges: on all three hosts--reld-identityprints reld's version with no child process (Phase 5)Gate
flag_corpusjob green on Linux, Windows, macOS with the full D4 matrix and lock file (Phase 6)mainruns (Phase 6)docs/flags.md, README,polylinker.mdregenerated from the table with no manual edits (Phase 6)Acceptance for this issue
Ok(())no-op flag handler exists outside the flag table; every table entry has a disposition with a test or probe reference; every entry in the vendored lld and GNU ld inventories has a rule.+crt-static, and-C link-dead-codelinks run natively underRELD_REQUIRE_ENGINE=reldwith correct semantics;-C lto,-C linker-plugin-lto, and bitcode inputs route to lld underRELD_REQUIRE_ENGINE=lld.SHF_GNU_RETAINand.deplibsare honored natively with tests.-z textand-l:namework.Native; the semantic-group ratchet count is published by the Phase 6 job.nix_rpath.rspasses underRELD_ENGINE=lld; no reld-only option reaches a child linker; a signal-killed child is reported as a signal.reld --reld-identitynever bridges.clangprobe, and identity.Related
8420e6c1; superseded here), Inventory: native engine gaps (routed to lld / ignored / unsupported) #120 (gap inventory; this issue is its resolution plan)docs/plan/01-DECISIONS.mdD13agents/docs/polylinker.md, andagents/docs/routing-maintenance.md(the contributor procedure for promoting/demoting a capability and the stderr policy; merged in docs(agents): routing-maintenance guide for promoting/demoting native capabilities #126)