From cde5b9881cf073f9aadbc6a6b413429f09d57b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 15 Aug 2026 07:27:29 +0200 Subject: [PATCH 1/4] fix(test): keep the runtime's symbols global in the ELF provider link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gc-native-roots` is red on main, on both ELF platforms only, with Error: "stdlib provider is bound to a different runtime image" Mach-O and Windows pass. Bisected by re-running the last-good job on its own commit: 5c27d1ad9 passes today, so the environment is fine and the gate is not flaky. The break is the next commit, #8089, which added the ELF branch of this linker shim. That branch writes a version script listing the 16 provider exports and then `local: *`. The provider statically links the runtime rlib as well as loading the runtime `.so`, so it carries its own definition of `js_gc_init` and friends, and `local: *` binds those internally — a local symbol is not preemptible. The stdlib therefore stops resolving stateful runtime calls to the image the host loaded first, which is exactly the condition this fixture exists to detect, and it duly detected it. The Mach-O branch never had the problem because it re-exports the runtime's whole symbol set (`nm -gU "$runtime_library"`) alongside the provider exports. Do the same for ELF. `local: *` still hides everything else, so #8089's intent — export the Web Fetch/Streams surface the later-loaded app needs, and nothing more — is preserved. `nm -D --defined-only` rather than the Mach-O branch's `nm -gU`: preemption is governed by the dynamic symbol table, not the static symtab that a stripped `.so` need not carry at all. --- .../issue_8075_provider_gc/stdlib-linker.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh index cccf01c73e..39384fd29e 100755 --- a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh +++ b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh @@ -87,6 +87,22 @@ if [[ -n "$original_version_script" ]]; then { echo '{ global:' printf ' %s;\n' "${stdlib_provider_exports[@]}" + # The runtime's own symbols must stay GLOBAL here, exactly as the + # Mach-O branch above re-exports them. + # + # This provider statically links the runtime rlib as well as loading the + # runtime .so, so it carries its own definition of `js_gc_init` and + # friends. `local: *` binds those internally and makes them + # non-preemptible, so the stdlib stops resolving stateful runtime calls + # to the image the host loaded first — which is the exact thing this + # fixture exists to detect, and it reported it as + # "stdlib provider is bound to a different runtime image" (#8089). + # `-D`: the DYNAMIC symbol table. This branch is ELF-only, and what + # governs preemption is what the dynamic linker sees, not the static + # symtab a stripped .so need not carry at all. (The Mach-O branch's + # plain `nm -gU` is right for its format, where the dylib's table is + # what plain nm reads.) + nm -D --defined-only "$runtime_library" | awk 'NF >= 3 { printf " %s;\n", $3 }' echo 'local: *; };' } > "$custom_version_script" arguments+=("-Wl,--version-script=$custom_version_script") From 834e83aec2472df6611dd0370ac5c5aa5ea9b8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 15 Aug 2026 07:27:46 +0200 Subject: [PATCH 2/4] chore(changelog): add fragment for #8127 --- .../8127-elf-provider-runtime-exports.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 changelog.d/8127-elf-provider-runtime-exports.md diff --git a/changelog.d/8127-elf-provider-runtime-exports.md b/changelog.d/8127-elf-provider-runtime-exports.md new file mode 100644 index 0000000000..326e8c9c4b --- /dev/null +++ b/changelog.d/8127-elf-provider-runtime-exports.md @@ -0,0 +1,19 @@ +`gc-native-roots` is green on ELF again. The gate failed on both Linux +platforms — Mach-O and Windows passed — with "stdlib provider is bound to a +different runtime image". Re-running the last-good job on its own commit shows +that commit still passes, so the environment was never at fault: the break came +with the ELF branch of the #8075 linker shim added in #8089. + +That branch wrote a version script listing the 16 provider exports followed by +`local: *`. The provider statically links the runtime rlib as well as loading +the runtime `.so`, so it carries its own `js_gc_init` and friends; `local: *` +binds those internally, and a local symbol is not preemptible. The stdlib +therefore stopped resolving stateful runtime calls to the image the host loaded +first — the exact condition the fixture exists to detect. + +The ELF version script now keeps the runtime's symbols global, as the Mach-O +branch already did by re-exporting `nm`'s view of the runtime library. +`local: *` still hides everything else, so #8089's intent — export the Web +Fetch/Streams surface the later-loaded app needs and nothing more — is +unchanged. ELF reads the DYNAMIC symbol table (`nm -D --defined-only`), since +preemption is governed by what the dynamic linker sees. From cb7b49abf4aef171c47072c454c3702d9c1d0b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 15 Aug 2026 15:50:34 +0200 Subject: [PATCH 3/4] fix(test): stop hiding the runtime's symbols in the ELF provider link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second attempt; the first was wrong in a way CI caught. The failure is real: `gc-native-roots` is red on both ELF platforms only, with "stdlib provider is bound to a different runtime image". Re-running the last-good job on its own commit shows that commit still passes, so the environment is not at fault — the break came with the ELF branch of this shim added in #8089. The cause is the `local: *` in the version script that branch writes. The provider statically links the runtime rlib as well as loading the runtime `.so`, so it carries its own `js_gc_init` and friends; `local: *` binds those internally, and a local symbol is not preemptible. The stdlib then resolves stateful runtime calls to its OWN copy rather than the image the host loaded first — precisely the condition the fixture detects. Before this shim parsed `--version-script` at all, the rustc-generated script was passed through and the gate passed: the regression is the hiding, not the export list. My first fix mirrored the Mach-O branch and re-exported the runtime's whole symbol table via `nm`. That cannot work here: rustc also passes `--no-undefined-version`, so naming a symbol the output does not define is a hard lld error, and the link died with hundreds of "version script assignment of 'global' to symbol ... failed: symbol not defined". ld64 only warns, which is why the Mach-O branch gets away with it. Drop the `local: *` instead. Every symbol not named keeps its default global, preemptible binding — restoring the pre-#8089 behaviour — while the 16 provider exports are still named explicitly, so #8089's actual goal (the later-loaded app resolves the Web Fetch/Streams surface) is unchanged. --- .../issue_8075_provider_gc/stdlib-linker.sh | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh index 39384fd29e..56af7a153f 100755 --- a/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh +++ b/tests/fixtures/issue_8075_provider_gc/stdlib-linker.sh @@ -84,26 +84,27 @@ fi if [[ -n "$original_version_script" ]]; then if [[ "$saw_runtime_rlib" == true ]]; then custom_version_script=$(mktemp "${TMPDIR:-/tmp}/perry-8075-version.XXXXXX") + # `global:` WITHOUT a `local: *`. + # + # The provider statically links the runtime rlib as well as loading the + # runtime .so, so it carries its own `js_gc_init` and friends. `local: *` + # binds those internally, and a local symbol is not preemptible — the + # stdlib then resolves stateful runtime calls to its OWN copy instead of + # the image the host loaded first, which is exactly what this fixture + # exists to detect ("stdlib provider is bound to a different runtime + # image"). Before this shim parsed `--version-script` at all, the + # rustc-generated script was passed through and the gate passed; the + # regression came with the hiding, not with the export list. + # + # Listing the runtime's symbols explicitly is NOT the fix: rustc also + # passes `--no-undefined-version`, so naming a symbol the output does not + # define is a hard lld error. Omitting `local: *` leaves every other + # symbol at its default (global, preemptible) binding and names only what + # must be added. { echo '{ global:' printf ' %s;\n' "${stdlib_provider_exports[@]}" - # The runtime's own symbols must stay GLOBAL here, exactly as the - # Mach-O branch above re-exports them. - # - # This provider statically links the runtime rlib as well as loading the - # runtime .so, so it carries its own definition of `js_gc_init` and - # friends. `local: *` binds those internally and makes them - # non-preemptible, so the stdlib stops resolving stateful runtime calls - # to the image the host loaded first — which is the exact thing this - # fixture exists to detect, and it reported it as - # "stdlib provider is bound to a different runtime image" (#8089). - # `-D`: the DYNAMIC symbol table. This branch is ELF-only, and what - # governs preemption is what the dynamic linker sees, not the static - # symtab a stripped .so need not carry at all. (The Mach-O branch's - # plain `nm -gU` is right for its format, where the dylib's table is - # what plain nm reads.) - nm -D --defined-only "$runtime_library" | awk 'NF >= 3 { printf " %s;\n", $3 }' - echo 'local: *; };' + echo '};' } > "$custom_version_script" arguments+=("-Wl,--version-script=$custom_version_script") else From a1c478681c518ff74a723c35141a0b9c5919a3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sat, 15 Aug 2026 15:50:47 +0200 Subject: [PATCH 4/4] chore(changelog): correct the fragment for #8127 --- .../8127-elf-provider-runtime-exports.md | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/changelog.d/8127-elf-provider-runtime-exports.md b/changelog.d/8127-elf-provider-runtime-exports.md index 326e8c9c4b..ec04d3c6d8 100644 --- a/changelog.d/8127-elf-provider-runtime-exports.md +++ b/changelog.d/8127-elf-provider-runtime-exports.md @@ -4,16 +4,17 @@ different runtime image". Re-running the last-good job on its own commit shows that commit still passes, so the environment was never at fault: the break came with the ELF branch of the #8075 linker shim added in #8089. -That branch wrote a version script listing the 16 provider exports followed by -`local: *`. The provider statically links the runtime rlib as well as loading -the runtime `.so`, so it carries its own `js_gc_init` and friends; `local: *` -binds those internally, and a local symbol is not preemptible. The stdlib -therefore stopped resolving stateful runtime calls to the image the host loaded -first — the exact condition the fixture exists to detect. +The cause is that branch's `local: *`. The provider statically links the +runtime rlib as well as loading the runtime `.so`, so it carries its own +`js_gc_init` and friends; `local: *` binds those internally, and a local symbol +is not preemptible. The stdlib then resolved stateful runtime calls to its own +copy rather than the image the host loaded first — the exact condition the +fixture exists to detect. Before the shim parsed `--version-script` at all the +rustc-generated script was passed through and the gate passed, so the +regression is the hiding, not the export list. -The ELF version script now keeps the runtime's symbols global, as the Mach-O -branch already did by re-exporting `nm`'s view of the runtime library. -`local: *` still hides everything else, so #8089's intent — export the Web -Fetch/Streams surface the later-loaded app needs and nothing more — is -unchanged. ELF reads the DYNAMIC symbol table (`nm -D --defined-only`), since -preemption is governed by what the dynamic linker sees. +The version script now names the 16 provider exports with no `local: *`, so +every other symbol keeps its default global, preemptible binding. Re-exporting +the runtime's symbol table explicitly (as the Mach-O branch does via `nm`) is +not an option on ELF: rustc also passes `--no-undefined-version`, so naming a +symbol the output does not define is a hard lld error.