From ec59c787359945475cc0b445e4d517ab82f07a5f Mon Sep 17 00:00:00 2001 From: vad Date: Wed, 30 Sep 2026 03:47:33 +0200 Subject: [PATCH] BPF: Allow all libcalls By default, BPF backend in LLVM allows only `memcpy`, `memset` and `memmove` libcalls. However, `core/src/ops/arith.rs:932` emits an `__extendhfsf2` libcall. LLVM 23 provides an opt-in mode that lets BPF runtimes supply implementations for otherwise unsupported libcalls.[0] Enable it unconditionally instead of enforcing LLVM's fixed allowlist. This defers validation of libcalls (and rejection of undefined ones) to BPF program consumers such as loaders and VMs. libbpf rejects ordinary undefined functions without BTF metadata.[1] Aya generally rejects unresolved calls during relocation.[2] If LLVM's unresolved `call -1` placeholder reaches the Linux verifier unchanged,[3] its relative target is the call itself, so the verifier rejects it as recursion.[4] SBPF's ELF loader rejects external symbols absent from the loader's function registry when `reject_broken_elfs` option is enabled.[5] Agave enables it while deploying and upgrading programs.[6] Users can therefore satisfy a libcall through a linked implementation or a runtime-provided function, while genuinely unresolved calls still fail during loading, verification or execution. Runtimes can also add libcalls without coordinating an allowlist change with LLVM. Given that the opt-in flag for libcalls is available only in specific LLVM versions (>=23), add `llvm_args_by_version` field to `TargetOptions`, that allows to specify LLVM arguments by version. Code in this change is generated by (an open-weight and self-hosted) LLM, it was agreed on in the Zulip thread [7], where @nagisa volunteered as a reviewer. [0] https://github.com/llvm/llvm-project/commit/8446b9ffb [1] https://github.com/llvm/llvm-project/pull/199542 [2] https://github.com/libbpf/libbpf/blob/f7081a/src/libbpf.c#L4345-L4350 [3] https://github.com/aya-rs/aya/blob/15a0de/aya-obj/src/relocation.rs#L606-L614 [4] https://github.com/torvalds/linux/blob/f5bbbfec5/kernel/bpf/verifier.c#L2798-L2800 [5] https://github.com/torvalds/linux/blob/f5bbbfec5/kernel/bpf/verifier.c#L2952-L2958 [6] https://github.com/anza-xyz/sbpf/blob/45bbcb/src/elf.rs#L1223-L1227 [7] https://rust-lang.zulipchat.com/#narrow/channel/606558-llm-reviews/topic/Seeking.20review.20for.20BPF.20bytecode.20linking.20.28issue.20.23135175.29/near/631892496 --- compiler/rustc_codegen_llvm/src/llvm_util.rs | 8 ++- compiler/rustc_target/src/spec/base/bpf.rs | 6 ++- compiler/rustc_target/src/spec/json.rs | 3 ++ compiler/rustc_target/src/spec/mod.rs | 9 ++++ compiler/rustc_target/src/tests.rs | 56 +++++++++++++++++++- tests/assembly-llvm/bpf-libcall.rs | 19 +++++++ 6 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 tests/assembly-llvm/bpf-libcall.rs diff --git a/compiler/rustc_codegen_llvm/src/llvm_util.rs b/compiler/rustc_codegen_llvm/src/llvm_util.rs index b67f6ca851177..2b547e5502842 100644 --- a/compiler/rustc_codegen_llvm/src/llvm_util.rs +++ b/compiler/rustc_codegen_llvm/src/llvm_util.rs @@ -51,9 +51,9 @@ unsafe fn configure_llvm(sess: &EarlySession) { let mut llvm_c_strs = Vec::with_capacity(n_args + 1); let mut llvm_args = Vec::with_capacity(n_args + 1); + let (llvm_major, llvm_minor, llvm_patch) = get_version(); // Check to ensure we're running against the correct LLVM version. unsafe { - let (llvm_major, llvm_minor, llvm_patch) = get_version(); let expected_version = llvm::LLVMRustVersionMajor(); if llvm_major != expected_version { sess.dcx().emit_fatal(diagnostics::LlvmVersionMismatch { @@ -127,10 +127,14 @@ unsafe fn configure_llvm(sess: &EarlySession) { } } - if get_version() < (24, 0, 0) && wants_wasm_eh(&sess.target) { + if llvm_major < 24 && wants_wasm_eh(&sess.target) { add("-wasm-enable-eh", false); } + for arg in sess.target.llvm_args_for_llvm_version(llvm_major) { + add(arg.as_ref(), false); + } + // HACK(eddyb) LLVM inserts `llvm.assume` calls to preserve align attributes // during inlining. Unfortunately these may block other optimizations. add("-preserve-alignment-assumptions-during-inlining=false", false); diff --git a/compiler/rustc_target/src/spec/base/bpf.rs b/compiler/rustc_target/src/spec/base/bpf.rs index 8d541cb78deac..95bf9494de733 100644 --- a/compiler/rustc_target/src/spec/base/bpf.rs +++ b/compiler/rustc_target/src/spec/base/bpf.rs @@ -1,8 +1,11 @@ +use std::collections::BTreeMap; + use rustc_abi::Endian; -use crate::spec::{LinkerFlavor, MergeFunctions, PanicStrategy, TargetOptions}; +use crate::spec::{LinkerFlavor, MergeFunctions, PanicStrategy, StaticCow, TargetOptions, cvs}; pub(crate) fn opts(endian: Endian) -> TargetOptions { + let allows_libcalls: StaticCow<[StaticCow]> = cvs!["-bpf-allows-libcalls"]; TargetOptions { allow_asm: true, endian, @@ -24,6 +27,7 @@ pub(crate) fn opts(endian: Endian) -> TargetOptions { // and would require a bit of a refactor. min_atomic_width: Some(64), max_atomic_width: Some(64), + llvm_args_by_version: BTreeMap::from([(23, allows_libcalls)]), ..Default::default() } } diff --git a/compiler/rustc_target/src/spec/json.rs b/compiler/rustc_target/src/spec/json.rs index 03dbbcb5b481d..57fa78bee6c36 100644 --- a/compiler/rustc_target/src/spec/json.rs +++ b/compiler/rustc_target/src/spec/json.rs @@ -203,6 +203,7 @@ impl Target { forward_opt!(rustc_abi); forward!(relax_elf_relocations); forward!(llvm_args); + forward!(llvm_args_by_version); forward!(use_ctors_section); forward!(eh_frame_header); forward!(has_thumb_interworking); @@ -396,6 +397,7 @@ impl ToJson for Target { target_option_val!(rustc_abi); target_option_val!(relax_elf_relocations); target_option_val!(llvm_args); + target_option_val!(llvm_args_by_version); target_option_val!(use_ctors_section); target_option_val!(eh_frame_header); target_option_val!(has_thumb_interworking); @@ -619,6 +621,7 @@ struct TargetSpecJson { rustc_abi: Option, relax_elf_relocations: Option, llvm_args: Option]>>, + llvm_args_by_version: Option]>>>, use_ctors_section: Option, eh_frame_header: Option, has_thumb_interworking: Option, diff --git a/compiler/rustc_target/src/spec/mod.rs b/compiler/rustc_target/src/spec/mod.rs index ae17e37e29f86..200ba166d09dd 100644 --- a/compiler/rustc_target/src/spec/mod.rs +++ b/compiler/rustc_target/src/spec/mod.rs @@ -2550,6 +2550,7 @@ pub struct TargetOptions { /// Additional arguments to pass to LLVM, similar to the `-C llvm-args` codegen option. pub llvm_args: StaticCow<[StaticCow]>, + pub llvm_args_by_version: BTreeMap]>>, /// Whether to use legacy .ctors initialization hooks rather than .init_array. Defaults /// to false (uses .init_array). @@ -2835,6 +2836,7 @@ impl Default for TargetOptions { rustc_abi: None, relax_elf_relocations: false, llvm_args: cvs![], + llvm_args_by_version: BTreeMap::new(), use_ctors_section: false, eh_frame_header: true, has_thumb_interworking: false, @@ -2893,6 +2895,13 @@ impl Target { self.max_atomic_width.unwrap_or_else(|| self.pointer_width.into()) } + pub fn llvm_args_for_llvm_version<'a>( + &'a self, + major: u32, + ) -> impl Iterator> { + self.llvm_args_by_version.range(..=major).flat_map(|(_, args)| args.iter()) + } + /// Test target self-consistency and JSON encoding/decoding roundtrip. #[cfg(test)] fn test_target(mut self) { diff --git a/compiler/rustc_target/src/tests.rs b/compiler/rustc_target/src/tests.rs index bb17dd151b58d..3cdbc13fc429e 100644 --- a/compiler/rustc_target/src/tests.rs +++ b/compiler/rustc_target/src/tests.rs @@ -1,5 +1,8 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; + use crate::json::ToJson; -use crate::spec::Target; +use crate::spec::{Target, TargetTuple}; #[test] fn report_unused_fields() { @@ -40,3 +43,54 @@ fn custom_arch_propagates_from_json() { assert_eq!(serialized["arch"], "customarch"); }); } + +fn bpf_target_with_versioned_args(map: BTreeMap]>>) -> Target { + let mut t = Target::expect_builtin(&TargetTuple::from_tuple("bpfeb-unknown-none")); + t.options.llvm_args_by_version = map; + t +} + +#[test] +fn llvm_args_by_version_empty_map_returns_no_args() { + let t = bpf_target_with_versioned_args(BTreeMap::new()); + assert!(t.llvm_args_for_llvm_version(1).collect::>().is_empty()); + assert!(t.llvm_args_for_llvm_version(9999).collect::>().is_empty()); +} + +#[test] +fn llvm_args_by_version_excludes_entries_above_major() { + let t = bpf_target_with_versioned_args(BTreeMap::from([ + (23u32, Cow::Borrowed(&[Cow::Borrowed("-a")][..])), + (24, Cow::Borrowed(&[Cow::Borrowed("-b")][..])), + ])); + assert!(t.llvm_args_for_llvm_version(22).collect::>().is_empty()); + assert_eq!(t.llvm_args_for_llvm_version(23).collect::>(), &[&Cow::Borrowed("-a")]); + assert_eq!( + t.llvm_args_for_llvm_version(24).collect::>(), + &[&Cow::Borrowed("-a"), &Cow::Borrowed("-b")] + ); + assert_eq!( + t.llvm_args_for_llvm_version(25).collect::>(), + &[&Cow::Borrowed("-a"), &Cow::Borrowed("-b")] + ); +} + +#[test] +fn llvm_args_by_version_accumulates_multiple_entries() { + let t = bpf_target_with_versioned_args(BTreeMap::from([ + (23u32, Cow::Borrowed(&[Cow::Borrowed("-a"), Cow::Borrowed("-b")][..])), + (24, Cow::Borrowed(&[Cow::Borrowed("-c")][..])), + ])); + assert_eq!( + t.llvm_args_for_llvm_version(23).collect::>(), + &[&Cow::Borrowed("-a"), &Cow::Borrowed("-b")] + ); + assert_eq!( + t.llvm_args_for_llvm_version(24).collect::>(), + &[&Cow::Borrowed("-a"), &Cow::Borrowed("-b"), &Cow::Borrowed("-c")] + ); + assert_eq!( + t.llvm_args_for_llvm_version(100).collect::>(), + &[&Cow::Borrowed("-a"), &Cow::Borrowed("-b"), &Cow::Borrowed("-c")] + ); +} diff --git a/tests/assembly-llvm/bpf-libcall.rs b/tests/assembly-llvm/bpf-libcall.rs new file mode 100644 index 0000000000000..7133c08aa7474 --- /dev/null +++ b/tests/assembly-llvm/bpf-libcall.rs @@ -0,0 +1,19 @@ +//@ add-minicore +//@ revisions: el eb +//@ assembly-output: emit-asm +//@ [el] compile-flags: --target bpfel-unknown-none -Copt-level=0 +//@ [eb] compile-flags: --target bpfeb-unknown-none -Copt-level=0 +//@ needs-llvm-components: bpf +//@ min-llvm-version: 23 +#![feature(no_core, f16)] +#![no_core] +#![crate_type = "lib"] + +extern crate minicore; + +// CHECK-LABEL: f16_to_f32: +// CHECK: call __extendhfsf2 +#[no_mangle] +pub fn f16_to_f32(x: f16) -> f32 { + x as f32 +}