Skip to content

make tests/run-make/used-proc-macro more reliable#155436

Open
japaric wants to merge 1 commit intorust-lang:mainfrom
ferrocene:gh155434
Open

make tests/run-make/used-proc-macro more reliable#155436
japaric wants to merge 1 commit intorust-lang:mainfrom
ferrocene:gh155434

Conversation

@japaric
Copy link
Copy Markdown
Contributor

@japaric japaric commented Apr 17, 2026

the test was relying on the linker not discarding the #[used] symbol

this commit modifies the proc macro to read the #[used] symbol to ensure it is not discarded by the linker

fixes #155434

@rustbot rustbot added A-run-make Area: port run-make Makefiles to rmake.rs S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Apr 17, 2026
@rustbot
Copy link
Copy Markdown
Collaborator

rustbot commented Apr 17, 2026

r? @mejrs

rustbot has assigned @mejrs.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: compiler
  • compiler expanded to 72 candidates
  • Random selection from 19 candidates

@rustbot

This comment has been minimized.

the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker
Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 17, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
Copy link
Copy Markdown
Contributor

@mejrs mejrs left a comment

Choose a reason for hiding this comment

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

Comment on lines 3 to +4
#[used]
#[no_mangle] // so we can refer to this variable by name from the proc-macro library
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Doesn't #[no_mangle] imply #[used]?

This is not my area of expertise but it looks like you can drop the #[used] attribute (and adjust the comments appropriately). Please correct me if I'm wrong.

Copy link
Copy Markdown
Member

@bjorn3 bjorn3 Apr 17, 2026

Choose a reason for hiding this comment

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

Yeah, #[used] would no longer have much of an effect now.

Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 17, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
@mejrs mejrs added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Apr 17, 2026
Comment on lines +14 to +19
// read the symbol otherwise the _linker_ may discard it
// `#[used]` only preserves the symbol up to the compiler output (.o file)
// which is then passed to the linker. the linker is free to discard the
// `#[used]` symbol if it's not accessed/referred-to by other object
// files (crates)
let symbol_value = unsafe { core::ptr::addr_of!(VERY_IMPORTANT_SYMBOL).read_volatile() };
Copy link
Copy Markdown
Member

@bjorn3 bjorn3 Apr 17, 2026

Choose a reason for hiding this comment

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

#[used] is an alias for #[used(linker)] since #140872, which should prevent the linker from discarding it. In either case now that you added a reference to it in another crate, this test is no longer testing #[used] as it should. The linker error that you observed is a genuine bug that this test caught. This bug should be fixed instead of changing the test.

View changes since the review

Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 18, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 18, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 18, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 18, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 18, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 18, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 18, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 18, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
Xylakant pushed a commit to ferrocene/ferrocene that referenced this pull request Apr 18, 2026
the test was relying on the linker not discarding the `#[used]` symbol

this commit modifies the proc macro to read the `#[used]` symbol to ensure it
is not discarded by the linker

cherry-picked from upstream, see rust-lang/rust#155436
@japaric
Copy link
Copy Markdown
Contributor Author

japaric commented Apr 20, 2026

@bjorn3

The linker error that you observed is a genuine bug that this test caught. This bug should be fixed instead of changing the test.

after looking further into this, I think whether the symbol ends in the final artifact may still be linker dependent.

As I understand the underlying mechanism, the compiler creates a symbols.o that contains "undefined" (U) references to symbols marked #[no_mangle] and #[used(linker)] which then passes to the linker as an early argument.

the linker is supposed to exhaustively scan the input files (linker arguments) until it finds those undefined symbols in symbols.o and retain them in the final artifact. however, that's where the observed behavior is linker dependent.

on revision 27dbdb5

  • with profile = 'dist' set in bootstrap.toml I see the used-proc-macro test pass on x86_64 Linux. this configuration uses rust-lld as the linker by way of passing -fused-linker=lld to GCC

  • with profile = 'dist' + target.x86_64-unknown-linux-gnu.default-linker-linux-override = 'off' and GCC 11.4.0 (Ubuntu 22.04) on x86_64 the test also passes. this "off" setup uses the system's GNU LD, not rust-lld.

  • with profile = 'dist' + target.x86_64-unknown-linux-gnu.default-linker-linux-override = 'off' and GCC 9.4.0 (Ubuntu 20.04) on x86_64 the test fails as the symbol is discarded by the linker.

  • with profile = 'dist' + target.x86_64-unknown-linux-gnu.default-linker-linux-override = 'off' and GCC 10.5.0 (Ubuntu 20.04 with gcc-10 package) on x86_64 the test also fails as the symbol is discarded by the linker.

I haven't tested other GCC versions so I can't say for sure that this is just a bug on older GCC versions. However, manually discarding the rlib from the linker invocation to keep the linker from finding the "undefined" symbol does not produce a linker error with either LLD or GNU LD (LLD repro below) so leaving some symbols "undefined" appears to not be treated as a fatal error, by either linker, that warrants aborting the linking process. I tried a few linker flags that should turn "unresolved undefined" symbols into hard errors but only --require-defined=the-name-of-the-symbol worked and that's GNU LD specific (not present in LLD's CLI).

The only other alternative to symbols.o that I'm aware of that preserves symbols during linking is to use EXTERN or KEEP in a linker script but that's specific to ELF files.

Details
$ cargo new --bin repro
$ cd repro
$ cargo new --lib dep
$ cat dep/src/lib.rs
#![feature(used_with_arg)]
#![no_std]

#[used(linker)]
static VERY_IMPORTANT_SYMBOL: usize = 42;
$ cat Cargo.toml
[package]
name = "repro"
edition = "2024"

[dependencies]
dep.path = "dep"
$ cat src/main.rs
#![no_main]
#![no_std]

use dep as _;

#[unsafe(no_mangle)]
fn _start() {}

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

NOTE: the compilation target is not really important but a no_std program keeps the number of involved artifacts and symbols small

$ cargo clean
$ cargo rustc --target aarch64-unknown-none -v -- --print link-args -C save-temps
$ nm -CSn target/aarch64-unknown-none/debug/deps/rust*/symbols.o
                 U __rustc::rust_begin_unwind
                 U __rustc::rust_begin_unwind
                 U dep::VERY_IMPORTANT_SYMBOL
                 U _start

$ nm -CSn target/aarch64-unknown-none/debug/repro | tail -n4
0000000000200158 0000000000000008 R dep::VERY_IMPORTANT_SYMBOL
0000000000200178 r $d
00000000002101a8 t $x
00000000002101a8 0000000000000004 T _start

repat the linker invocation printed by cargo rustc but discard the target/aarch64-unknown-none/debug/deps/libdep-*.rlib argument:

$ (..) "rust-lld" "-flavor" "gnu" (..) && echo OK
OK

$ nm -CSn target/aarch64-unknown-none/debug/deps/repro-* | tail -n4
000000000000017c N $d
0000000000200170 r $d
00000000002101a0 t $x
00000000002101a0 0000000000000004 T _start

The modified linker invocation can be repeated with GNU LD (e.g. aarch64-none-elf-ld). The result was the same; no linker error despite the unresolved "undefined" reference.

@mati865
Copy link
Copy Markdown
Member

mati865 commented Apr 20, 2026

The symbol is retained with recent linkers thanks to SHF_GNU_RETAIN flag.
If you check the assembly, this is clearer (Godbolt linkt):

        .section        .rodata.example[caa99c3a58b05bc]::VERY_IMPORTANT_SYMBOL,"aR",@progbits
        .globl  example[caa99c3a58b05bc]::VERY_IMPORTANT_SYMBOL

Note the uppercase R in section definition. This means the entire section won't be considered by linker's GC.

This functionality appears to be added to as in Binutils 2.36. I couldn't quickly find when it was added to ld.bfd but probably around the same date. When it comes to LLD, versions 13 and newer should handle it.

IMO the right call here would be to ignore the test if linked with ld.bfd < 2.36.

@japaric
Copy link
Copy Markdown
Contributor Author

japaric commented Apr 28, 2026

IMO the right call here would be to ignore the test if linked with ld.bfd < 2.36.

putting aside the technical detail of how to encode that condition in bootstrap or the run-make test itself, I think that if this language feature relies on the linker implementing certain features then that should be noted in the feature documentation.

the guarantee of the feature is that the symbol makes it to the output object. additionally, a hint is provided to the linker to also preserve the symbol but that behavior is not guaranteed. it can also be noted that ELF linkers that support SHF_GNU_RETAIN are known to respect the emitted hint.

after all, even if the default linker for a certain target is the rust-lld included in the toolchain, the user can still select a different linker via the command line which could then change the outcome of this attribute

@bjorn3
Copy link
Copy Markdown
Member

bjorn3 commented Apr 28, 2026

The language doesn't guarantee that #[used] is #[used(linker)] rather than #[used(compiler)], but this test does make this assumption. #[used(linker)] is a guarantee that the linker will preserve the section, not merely a hint. Older linkers that don't yet implement support for SHF_GNU_RETAIN may remove the section, but that is a problem with the linker, not a missing guarantee on the rust side. This is probably part of the reason #[used] hasn't been guaranteed as #[used(linker)] yet. I guess the test could be changed to explicitly use #[used(linker)]. For global constructors you only strictly need #[used(compiler)], but those require using a different section depending on the target, which would make the test less portable.

@japaric
Copy link
Copy Markdown
Contributor Author

japaric commented Apr 28, 2026

The language doesn't guarantee that #[used] is #[used(linker)] rather than #[used(compiler)], but this test does make this assumption.

Fair point about #[used]'s specified behavior; then so long as that remains the case this test should ... use (no pun intended) #[used(linker)], not #[used]

#[used(linker)] is a guarantee that the linker will preserve the section, not merely a hint. Older linkers that don't yet implement support for SHF_GNU_RETAIN may remove the section, but that is a problem with the linker, not a missing guarantee on the rust side.

Hmm, I think we may have to agree to disagree. SHF_GNU_RETAIN is a "GNU extension" (says the GNU documentation), not part of the ELF specification like say SHF_ALLOC, so it being implemented by other linkers is not a given I'd think.

Putting aside that the feature is unstable and that it's perfectly fine that no thorough documentation is yet in place; as the end user, if the #[used(linker)] documentation says that it "guarantees" that the symbol survives the linker and that does not work with my linker, any linker of my choice, then I'm going to open a bug report that the feature is not working as intended.

If, instead, the documentation says that (a) the compiler provides the linker a hint and that the symbol surviving the linker is not guaranteed by the compiler, which does not have the final say on the linker selection -- the user has the final say; OR (b) that it's a requirement that an ELF linker that supports SHF_GNU_USED is chosen by the user for the guarantee to hold -- and that responsibility is on the user not the compiler -- then I think that'll prevent some bug reports around misunderstandings as well as end user frustration.

@mati865
Copy link
Copy Markdown
Member

mati865 commented Apr 28, 2026

I agree that linker requirements should be documented, especially given Binutils 2.36 is old but not yet ancient. But then I think #[used(linker)] doesn't have any documentation to begin with, or at least it's not indexed by search engines.
Also, the support will probably vary by platform.

@bjorn3
Copy link
Copy Markdown
Member

bjorn3 commented Apr 28, 2026

SHF_GNU_RETAIN is a "GNU extension" (says the GNU documentation), not part of the ELF specification like say SHF_ALLOC, so it being implemented by other linkers is not a given I'd think.

There are a ton of things that are officially GNU extensions of the ELF spec, yet are mandatory on Linux according to the LSB. Both major C++ runtimes on Linux depend on STB_GNU_UNIQUE, everyone uses DT_GNU_HASH for the dylib symbol table rather than the ELF spec mandated DT_HASH. Unwind tables are located using PT_GNU_EH_FRAME. Symbol versioning is also a GNU extension, yet required to link against glibc. See https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/elf-generic.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-run-make Area: port run-make Makefiles to rmake.rs S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tests/run-make/used-proc-macro is flaky as it relies on linker behavior

5 participants