Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 192 additions & 4 deletions rust/private/rustc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,184 @@ def _are_linkstamps_supported(feature_configuration):
# Is Bazel recent enough to support Starlark linkstamps?
hasattr(cc_common, "register_linkstamp_compile_action"))

def _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode):
_NON_PIE_OS_KEYWORDS = [
"windows",
"uefi",
"vxworks",
"solaris",
"illumos",
"helenos",
"cygwin",
"l4re",
"lynxos178",
"aix",
"cuda",
]

_NON_PIE_ARCH_PREFIXES = [
"wasm",
"xtensa",
"nvptx",
"amdgcn",
"msp430",
"avr",
]

_NON_PIE_ENV_KEYWORDS = [
"nuttx",
"espidf",
"rtems",
"xous",
"zkvm",
"qurt",
"psp",
"psx",
"vita",
"3ds",
"vex",
]

def parse_rustc_version(version_semver):
"""Convert a parsed semver struct into the (major, minor, patch) tuple used for version comparisons.

A `None` input (channel labels like "nightly"/"beta" and unset versions
don't parse as semver) returns a sentinel representing "latest", since
nightly and beta are always at or ahead of the most recent stable release.

Args:
version_semver: A semver struct from `rust/private/semver.bzl`, typically
`toolchain.version_semver`, or `None`.

Returns:
A (major, minor, patch) tuple of ints.
"""
if not version_semver:
return (999, 0, 0)
return (version_semver.major, version_semver.minor, version_semver.patch)

def produces_pie_binaries(target_triple, rust_version):
"""Returns True if rustc links binaries as Position Independent Executables.

The truth table here is a port of the per-target `position_independent_executables`
field that rustc sets in its target specs. The constants and special cases
below were derived from analyzing
https://github.com/rust-lang/rust/tree/1.95.0/compiler/rustc_target/src/spec/base
across Rust 1.0.0-1.95.0.

## When to update

Add or revise an entry here whenever any of the following happens upstream:

- A new Rust release flips `position_independent_executables` on or off for
some target (most commonly via a base spec like `linux_musl_base.rs`).
- A new target triple is stabilized.
- rustc's PIE default itself changes (rare, but happens — e.g. the bpf
targets gained PIE in 1.75.0).

A divergence here typically shows up as a link error: rustc-emitted PIC/PIE
objects mixed with non-PIE intermediate rlibs (or vice versa).

## How to perform the analysis

rustc target specs live under `compiler/rustc_target/src/spec/`. Each
triple has a `targets/<triple>.rs` that returns a `Target` whose `options`
field is usually built from one of the shared bases under `spec/base/`
(e.g. `linux_gnu_base.rs`, `apple_base.rs`, `freebsd_base.rs`).

The decision tree for a given triple is:

1. Open `compiler/rustc_target/src/spec/targets/<triple>.rs` and follow
which `base::*` it calls into.
2. In that base file, look for `position_independent_executables: true`
(or the field being overridden back to `false` after the base sets it).
Some triples override the base's value directly in their own
`targets/<triple>.rs` — check there too.
3. To find the Rust release where the value changed, `git log -p -S
'position_independent_executables' compiler/rustc_target/src/spec/`
in a rust-lang/rust checkout and cross-reference the commit's first
containing release tag (e.g. `git tag --contains <sha> | sort -V | head -1`).
4. Cross-check with the `arm-linux-androideabi`, `x86_64-unknown-haiku`,
and `nto` entries below — those are the existing examples of
version-gated PIE flips and are the right shape to copy.

Three buckets cover the bulk of the matrix without per-triple cases; only
add a special case here when a triple is genuinely an outlier:

- **`_NON_PIE_OS_KEYWORDS`** — operating systems whose base spec sets
PIE off. Matched against any `-` separated component of the triple
(i.e. exact-segment match). Add an entry if a *new OS* lands and its
base spec sets `position_independent_executables: false` (or omits it,
since the default is false).
- **`_NON_PIE_ARCH_PREFIXES`** — architectures whose base spec sets PIE
off. Matched as a prefix of the first triple component (the arch),
because variants like `wasm32`/`wasm64` and `nvptx64` share a base.
- **`_NON_PIE_ENV_KEYWORDS`** — embedded/environment markers whose base
spec sets PIE off. Substring match against the full triple, because
these often appear concatenated with the OS (e.g. `nuttx` in
`aarch64-nuttx-elf`).

Args:
target_triple: A parsed target triple struct (see `rust/platform/triple.bzl`),
typically `toolchain.target_triple`. Provides `arch`, `vendor`, `system`,
`abi`, and `str` fields.
rust_version: (major, minor, patch) tuple, e.g. (1, 93, 0).

Returns:
True if rustc will pass -pie to the linker for this target.
"""
if target_triple.vendor == "apple":
return True

if target_triple.system == "none":
if target_triple.arch in ("bpfeb", "bpfel"):
return rust_version >= (1, 75, 0)
if target_triple.str == "x86_64-unknown-none":
return True
return False

if target_triple.system in _NON_PIE_OS_KEYWORDS:
return False

for prefix in _NON_PIE_ARCH_PREFIXES:
if target_triple.arch.startswith(prefix):
return False

# Env keywords use substring match against the full triple string because
# they sometimes appear in 4+-segment triples (e.g. "rtems" in
# "armv7-unknown-trappist-rtems-eabihf") that don't land cleanly in any
# single struct field.
for keyword in _NON_PIE_ENV_KEYWORDS:
if keyword in target_triple.str:
return False
if "solid" in target_triple.str:
return False

if target_triple.str == "i686-unknown-haiku":
return False
if target_triple.vendor == "unikraft":
return False

if target_triple.str == "x86_64-unknown-haiku":
return rust_version >= (1, 29, 0)
if target_triple.str == "arm-linux-androideabi":
return rust_version >= (1, 1, 0)

if target_triple.abi == "musl" and target_triple.system == "linux":
if target_triple.arch.startswith("mips"):
return True
return rust_version >= (1, 21, 0)
if target_triple.system == "freebsd":
return rust_version >= (1, 8, 0)
if target_triple.system == "hermit":
return rust_version >= (1, 40, 0)
if target_triple.system == "redox":
return rust_version >= (1, 38, 0)
if target_triple.system == "nto":
return rust_version >= (1, 75, 0)

return True

def _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode, toolchain):
"""Whether or not [PIC][pic] should be enabled

[pic]: https://en.wikipedia.org/wiki/Position-independent_code
Expand All @@ -167,6 +344,8 @@ def _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation
feature_configuration (FeatureConfiguration): Feature configuration to be queried.
crate_type (str): A Rust target's crate type.
compilation_mode: The compilation mode.
toolchain (rust_toolchain): The current `rust_toolchain`, used to derive
the target triple and rustc version for PIE detection.

Returns:
bool: Whether or not [PIC][pic] should be enabled.
Expand All @@ -180,6 +359,15 @@ def _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation
return cc_toolchain.needs_pic_for_dynamic_libraries(feature_configuration = feature_configuration)
elif compilation_mode in ("fastbuild", "dbg"):
return True

# In opt mode, rustc links executables with -pie on most platforms.
# Any CC objects linked into a PIE binary must be PIC, including those
# embedded in intermediate rlibs, so this applies to all crate types.
# target_triple is None when a custom target JSON spec is used.
if toolchain.target_triple:
rust_version = parse_rustc_version(toolchain.version_semver)
if produces_pie_binaries(toolchain.target_triple, rust_version):
return True
return False

def _is_proc_macro(crate_info):
Expand Down Expand Up @@ -721,7 +909,7 @@ def collect_inputs(
linker_depset = cc_toolchain.linker_files()
compilation_mode = ctx.var["COMPILATION_MODE"]

use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_info.type, compilation_mode)
use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_info.type, compilation_mode, toolchain)

# Pass linker inputs only for linking-like actions, not for example where
# the output is rlib. This avoids quadratic behavior where transitive noncrates are
Expand Down Expand Up @@ -1253,7 +1441,7 @@ def construct_arguments(
compilation_mode = ctx.var["COMPILATION_MODE"]
if toolchain.target_arch not in ("wasm32", "wasm64"):
if output_dir:
use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_info.type, compilation_mode)
use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_info.type, compilation_mode, toolchain)
rpaths = _compute_rpaths(toolchain, output_dir, dep_info, use_pic)
else:
rpaths = depset()
Expand Down Expand Up @@ -2637,7 +2825,7 @@ def _add_native_link_flags(
if crate_type in ["lib", "rlib"]:
return

use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode)
use_pic = _should_use_pic(cc_toolchain, feature_configuration, crate_type, compilation_mode, toolchain)

make_link_flags, get_lib_name = _get_make_link_flag_funcs(
target_os = toolchain.target_os,
Expand Down
46 changes: 32 additions & 14 deletions rust/private/semver.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,49 @@
def semver(version):
"""Constructs a struct containing separated sections of a semantic version value.

Parses per [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html):
`MAJOR.MINOR.PATCH[-PRE-RELEASE][+BUILD-METADATA]`.

Args:
version (str): The semver value.

Returns:
struct:
- major (int): The semver's major component. E.g. `1` from `1.2.3`
- minor (int): The semver's minor component. E.g. `2` from `1.2.3`
- patch (int): The semver's patch component. E.g. `3` from `1.2.3`
- pre (optional str): The semver's pre component. E.g. `rc4` from `1.2.3-rc4` or None if absent.
- major (int): The semver's major component. E.g. `1` from `1.2.3`.
- minor (int): The semver's minor component. E.g. `2` from `1.2.3`.
- patch (int): The semver's patch component. E.g. `3` from `1.2.3`.
- pre (optional str): The semver's pre-release identifier. E.g. `rc4`
from `1.2.3-rc4`, `beta.1` from `1.0.0-beta.1+exp.sha`. `None` when
no `-` is present.
- build (optional str): The semver's build metadata identifier. E.g.
`exp.sha.5114f85` from `1.0.0-beta+exp.sha.5114f85`. `None` when no
`+` is present.
- str (str): The full string value of the semver.
"""
parts = version.split(".", 2)
if len(parts) < 3:
fail("Unexpected number of parts for semver value: {}".format(version))

major = parts[0]
minor = parts[1]
patch, split, pre = parts[2].partition("-")
if not split:
# Build metadata is everything after the first `+`. Per the spec, `+` cannot
# appear inside MAJOR.MINOR.PATCH or the pre-release identifier, so this is
# always a clean split.
core, plus, build = version.partition("+")
if not plus:
build = None

# Pre-release is everything after the first `-` in the core. Multiple dashes
# are allowed in the pre-release identifier itself (e.g. `1.2.3-alpha-test`),
# so we only split on the first one.
main, dash, pre = core.partition("-")
if not dash:
pre = None

parts = main.split(".")
if len(parts) != 3:
fail("Unexpected number of parts for semver value: {}".format(version))

return struct(
major = int(major),
minor = int(minor),
patch = int(patch),
major = int(parts[0]),
minor = int(parts[1]),
patch = int(parts[2]),
pre = pre,
build = build,
str = version,
)
19 changes: 19 additions & 0 deletions rust/toolchain.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ load(
_current_rustfmt_toolchain = "current_rustfmt_toolchain",
_rustfmt_toolchain = "rustfmt_toolchain",
)
load("//rust/private:semver.bzl", "semver")
load(
"//rust/private:utils.bzl",
"deduplicate",
Expand Down Expand Up @@ -370,6 +371,18 @@ def _experimental_use_cc_common_link(ctx):
def _require_explicit_unstable_features(ctx):
return ctx.attr.require_explicit_unstable_features[BuildSettingInfo].value

_DIGITS = "0123456789"

def _is_semver_string(version):
"""Whether `version` looks like a `MAJOR.MINOR.PATCH[-pre][+build]` semver string.

The `rust_toolchain.version` attribute also accepts channel labels like
`"nightly"` or `"beta"` (and is sometimes the empty string), neither of
which are valid input for `semver()`. This filter exists so we can populate
`version_semver` opportunistically.
"""
return version != "" and version[0] in _DIGITS

def _expand_flags(ctx, attr_name, targets, make_variables):
targets = deduplicate(targets)
expanded_flags = []
Expand Down Expand Up @@ -584,6 +597,11 @@ def _rust_toolchain_impl(ctx):
if cc_toolchain and cc_toolchain.all_files:
all_files_depsets.append(cc_toolchain.all_files)

# Parse the version string once so downstream rules can branch on the
# semver components without re-parsing. `None` for empty or non-semver
# values (e.g. unset, or channel labels like "nightly" without a version).
version_semver = semver(ctx.attr.version) if _is_semver_string(ctx.attr.version) else None

toolchain = platform_common.ToolchainInfo(
all_files = depset(transitive = all_files_depsets),
binary_ext = ctx.attr.binary_ext,
Expand Down Expand Up @@ -633,6 +651,7 @@ def _rust_toolchain_impl(ctx):
target_abi = target_abi,
target_triple = target_triple,
version = ctx.attr.version,
version_semver = version_semver,
require_explicit_unstable_features = _require_explicit_unstable_features(ctx),

# Experimental and incompatible flags
Expand Down
3 changes: 3 additions & 0 deletions test/unit/rustc_pie/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
load(":rustc_pie_test.bzl", "rustc_pie_test_suite")

rustc_pie_test_suite(name = "rustc_pie_test_suite")
Loading
Loading