Merge ARM64 feature properties and emit BTI PLTs

We used to merge only x86 properties in .note.gnu.property, so an ARM64
output lost GNU_PROPERTY_AARCH64_FEATURE_1_AND even if all its inputs were
compiled with -mbranch-protection. We now AND-merge it as GNU ld and lld do.
If the output is BTI-enabled, the PLT header and the PLT entries that may be
reached by an indirect branch start with `bti c` as lld's do, which makes PLT
entries 24 bytes long, and DT_AARCH64_BTI_PLT is set.
This commit is contained in:
Rui Ueyama committed 2026-10-01 10:09:13 +09:00
1 parent 4ba7792e43
commit f03b745952
11 files changed
+214 -43

No files matched your search

+5 -2
View File
@@ -1,6 +1,6 @@
//! `.dynamic`, information consumed by the dynamic linker.
use crate::chunks::ChunkHeader;
use crate::chunks::{ChunkHeader, note_property};
use crate::context::Context;
use crate::elf::*;
use crate::input_files::FileId;
@@ -230,6 +230,9 @@ fn for_each_entry<E: Target>(ctx: &Context<E>, mut define: impl FnMut(u32, u64))
if E::FAMILY == Family::Arm64 && contains_variant_pcs(ctx) {
define(DT_AARCH64_VARIANT_PCS, 0);
}
if note_property::is_bti(ctx) {
define(DT_AARCH64_BTI_PLT, 0);
}
// RISC-V has the same feature but with a different name.
if E::IS_RISCV && plt.symbols.iter().any(|&id| ctx.symbols[id].esym(ctx).riscv_variant_cc()) {
define(DT_RISCV_VARIANT_CC, 0);
@@ -243,7 +246,7 @@ fn for_each_entry<E: Target>(ctx: &Context<E>, mut define: impl FnMut(u32, u64))
// it's what it is.
define(
DT_PPC64_GLINK,
plt.hdr.shdr.sh_addr.get() + crate::chunks::plt::entry_offset::<E>(0) - 32,
plt.hdr.shdr.sh_addr.get() + crate::chunks::plt::entry_offset(ctx, 0) - 32,
);
}
+49 -21
View File
@@ -5,7 +5,7 @@ use std::collections::{BTreeMap, BTreeSet};
use crate::chunks::ChunkHeader;
use crate::context::Context;
use crate::elf::*;
use crate::target::Target;
use crate::target::{Family, Target};
// .note.gnu.property section contains an additional runtime information
// about ISA variant.
@@ -21,6 +21,11 @@ impl<E: Target> NotePropertySection<E> {
hdr.shdr.sh_addralign.set(E::WORD_SIZE as u64);
Self { hdr, contents: Vec::new() }
}
/// The merged value of a property, or 0 if the output doesn't have it.
pub fn get(&self, ty: u32) -> u32 {
self.contents.iter().find(|&&(key, _)| key == ty).map_or(0, |&(_, val)| val)
}
}
impl<E: Target> Default for NotePropertySection<E> {
@@ -33,8 +38,22 @@ fn entry_size<E: Target>() -> usize {
if E::IS_64 { 16 } else { 12 }
}
// Merges input files' .note.gnu.property values.
pub fn update_shdr<E: Target>(ctx: &mut Context<E>) {
/// Whether the output is marked as compatible with ARM64 Branch Target
/// Identification (BTI). The code of such a file is mapped to guarded pages
/// in which an indirect branch must land on a `bti` instruction. Therefore,
/// linker-synthesized code that may be reached by an indirect branch has
/// to start with one.
pub fn is_bti<E: Target>(ctx: &Context<E>) -> bool {
E::FAMILY == Family::Arm64
&& ctx.note_property.as_ref().is_some_and(|sec| {
sec.get(GNU_PROPERTY_AARCH64_FEATURE_1_AND) & GNU_PROPERTY_AARCH64_FEATURE_1_BTI != 0
})
}
// Merges input files' .note.gnu.property values. This has to be done
// before computing section sizes because the result affects the PLT
// format on ARM64.
pub fn construct<E: Target>(ctx: &mut Context<E>) {
// Obtain the list of keys
let files: Vec<&crate::input_files::ObjectFile<E>> =
ctx.objs.iter().filter(|file| !ctx.is_internal(file.id())).collect();
@@ -46,15 +65,21 @@ pub fn update_shdr<E: Target>(ctx: &mut Context<E>) {
// Merge values for each key
let mut map: BTreeMap<u32, u32> = BTreeMap::new();
for key in keys {
if (GNU_PROPERTY_X86_UINT32_AND_LO..=GNU_PROPERTY_X86_UINT32_AND_HI).contains(&key) {
if (E::IS_X86
&& (GNU_PROPERTY_X86_UINT32_AND_LO..=GNU_PROPERTY_X86_UINT32_AND_HI).contains(&key))
|| (E::FAMILY == Family::Arm64 && key == GNU_PROPERTY_AARCH64_FEATURE_1_AND)
{
// An AND feature is set if all input objects have the property and
// the feature.
map.insert(key, files.iter().fold(u32::MAX, |acc, f| acc & value(f, key)));
} else if (GNU_PROPERTY_X86_UINT32_OR_LO..=GNU_PROPERTY_X86_UINT32_OR_HI).contains(&key) {
} else if E::IS_X86
&& (GNU_PROPERTY_X86_UINT32_OR_LO..=GNU_PROPERTY_X86_UINT32_OR_HI).contains(&key)
{
// An OR feature is set if some input object has the feature.
map.insert(key, files.iter().fold(0, |acc, f| acc | value(f, key)));
} else if (GNU_PROPERTY_X86_UINT32_OR_AND_LO..=GNU_PROPERTY_X86_UINT32_OR_AND_HI)
.contains(&key)
} else if E::IS_X86
&& (GNU_PROPERTY_X86_UINT32_OR_AND_LO..=GNU_PROPERTY_X86_UINT32_OR_AND_HI)
.contains(&key)
{
// An OR-AND feature is set if all input object files have the property
// and some of them has the feature.
@@ -64,23 +89,26 @@ pub fn update_shdr<E: Target>(ctx: &mut Context<E>) {
}
}
if ctx.args.z_ibt {
*map.entry(GNU_PROPERTY_X86_FEATURE_1_AND).or_insert(0) |= GNU_PROPERTY_X86_FEATURE_1_IBT;
if E::IS_X86 {
if ctx.args.z_ibt {
*map.entry(GNU_PROPERTY_X86_FEATURE_1_AND).or_insert(0) |=
GNU_PROPERTY_X86_FEATURE_1_IBT;
}
if ctx.args.z_shstk {
*map.entry(GNU_PROPERTY_X86_FEATURE_1_AND).or_insert(0) |=
GNU_PROPERTY_X86_FEATURE_1_SHSTK;
}
*map.entry(GNU_PROPERTY_X86_ISA_1_NEEDED).or_insert(0) |= ctx.args.z_x86_64_isa_level;
}
if ctx.args.z_shstk {
*map.entry(GNU_PROPERTY_X86_FEATURE_1_AND).or_insert(0) |= GNU_PROPERTY_X86_FEATURE_1_SHSTK;
}
*map.entry(GNU_PROPERTY_X86_ISA_1_NEEDED).or_insert(0) |= ctx.args.z_x86_64_isa_level;
// Serialize the map
let contents: Vec<(u32, u32)> = map.into_iter().filter(|&(_, v)| v != 0).collect();
ctx.note_property.as_mut().unwrap().contents =
map.into_iter().filter(|&(_, v)| v != 0).collect();
}
pub fn update_shdr<E: Target>(ctx: &mut Context<E>) {
let sec = ctx.note_property.as_mut().unwrap();
sec.hdr.shdr.sh_size.set(if contents.is_empty() {
0
} else {
(16 + contents.len() * entry_size::<E>()) as u64
});
sec.contents = contents;
let n = sec.contents.len();
sec.hdr.shdr.sh_size.set(if n == 0 { 0 } else { (16 + n * entry_size::<E>()) as u64 });
}
pub fn copy_buf<E: Target>(ctx: &Context<E>, buf: &mut [u8]) {
+12 -5
View File
@@ -2,7 +2,7 @@
use rayon::prelude::*;
use crate::chunks::ChunkHeader;
use crate::chunks::{ChunkHeader, note_property};
use crate::context::Context;
use crate::elf::*;
use crate::input_files::SymtabBlock;
@@ -44,8 +44,15 @@ impl<E: Target> Default for PltSection<E> {
// it switches to a "large" entry format. This is how many small entries fit.
pub const SPARC_NUM_SMALL_PLT: u64 = (0x100000 - 128) / 32;
/// The size of a PLT entry, for targets whose entries are uniformly sized.
pub fn entry_size<E: Target>(ctx: &Context<E>) -> u64 {
// ARM64 PLT entries grow from 16 to 24 bytes to make room for a `bti c`
// landing pad if BTI is enabled.
if note_property::is_bti(ctx) { 24 } else { E::PLT_SIZE }
}
/// The offset of a PLT entry within `.plt`.
pub fn entry_offset<E: Target>(idx: u32) -> u64 {
pub fn entry_offset<E: Target>(ctx: &Context<E>, idx: u32) -> u64 {
let idx = idx as u64;
match E::FAMILY {
Family::Ppc64V1 => {
@@ -71,7 +78,7 @@ pub fn entry_offset<E: Target>(idx: u32) -> u64 {
0x100000 + (i / 160) * 5120 + (i % 160) * 24
}
}
_ => E::PLT_HDR_SIZE + idx * E::PLT_SIZE,
_ => E::PLT_HDR_SIZE + idx * entry_size(ctx),
}
}
@@ -92,14 +99,14 @@ pub fn update_shdr<E: Target>(ctx: &mut Context<E>) {
} else if E::IS_SPARC {
E::PLT_HDR_SIZE + n * E::PLT_SIZE
} else {
entry_offset::<E>(n as u32)
entry_offset(ctx, n as u32)
});
}
pub fn copy_buf<E: Target>(ctx: &Context<E>, buf: &mut [u8]) {
E::write_plt_header(ctx, buf);
for (i, &id) in ctx.plt.symbols.iter().enumerate() {
let off = entry_offset::<E>(i as u32) as usize;
let off = entry_offset(ctx, i as u32) as usize;
E::write_plt_entry(ctx, &mut buf[off..], &ctx.symbols[id]);
}
}
+4
View File
@@ -334,6 +334,10 @@ pub fn link<E: Target>(cmdline: Cmdline) -> LinkResult {
if ctx.args.z_execstack_if_needed && ctx.objs.iter().any(|f| f.needs_executable_stack) {
ctx.args.z_execstack = true;
}
// Merge .note.gnu.property contents of input files.
if ctx.note_property.is_some() {
chunks::note_property::construct(&mut ctx);
}
// If we are linking a .so file, remaining undefined symbols does
// not cause a linker error. Instead, they are treated as if they
+4
View File
@@ -219,6 +219,7 @@ pub const DT_VERNEEDNUM: u32 = 0x6fffffff;
pub const DT_PPC_GOT: u32 = 0x70000000;
pub const DT_PPC64_GLINK: u32 = 0x70000000;
pub const DT_RISCV_VARIANT_CC: u32 = 0x70000001;
pub const DT_AARCH64_BTI_PLT: u32 = 0x70000001;
pub const DT_AARCH64_VARIANT_PCS: u32 = 0x70000005;
pub const DT_AUXILIARY: u32 = 0x7ffffffd;
pub const DT_FILTER: u32 = 0x7fffffff;
@@ -263,6 +264,9 @@ pub const GNU_PROPERTY_X86_ISA_1_V2: u32 = 2;
pub const GNU_PROPERTY_X86_ISA_1_V3: u32 = 4;
pub const GNU_PROPERTY_X86_ISA_1_V4: u32 = 8;
pub const GNU_PROPERTY_AARCH64_FEATURE_1_AND: u32 = 0xc0000000;
pub const GNU_PROPERTY_AARCH64_FEATURE_1_BTI: u32 = 1;
pub const ELFCOMPRESS_NONE: u32 = 0;
pub const ELFCOMPRESS_ZLIB: u32 = 1;
pub const ELFCOMPRESS_ZSTD: u32 = 2;
+1 -1
View File
@@ -171,7 +171,7 @@ pub fn create_synthetic_sections<E: Target>(ctx: &mut Context<E>) {
chunks.push(ChunkId::Verneed);
chunks.push(ChunkId::NotePackage);
if E::IS_X86 {
if E::IS_X86 || E::FAMILY == Family::Arm64 {
ctx.note_property = Some(NotePropertySection::<E>::new());
chunks.push(ChunkId::NoteProperty);
}
+5 -2
View File
@@ -43,7 +43,7 @@ use crate::input_files::FileId;
use crate::input_sections::InputSectionId;
use crate::output_file::OutputFile;
use crate::passes;
use crate::target::Target;
use crate::target::{Family, Target};
use crate::util::align_to;
/// An output section's sh_link can refer to only one section, so
@@ -109,7 +109,7 @@ fn create_synthetic_sections<E: Target>(ctx: &mut Context<E>) {
ChunkId::Symtab,
ChunkId::Shstrtab,
]);
if E::IS_X86 {
if E::IS_X86 || E::FAMILY == Family::Arm64 {
ctx.note_property = Some(NotePropertySection::<E>::new());
ctx.chunks.push(ChunkId::NoteProperty);
}
@@ -214,6 +214,9 @@ pub fn combine_objects<E: Target>(ctx: &mut Context<E>) {
split_link_order_sections(ctx);
create_synthetic_sections(ctx);
claim_unresolved_symbols(ctx);
if ctx.note_property.is_some() {
chunks::note_property::construct(ctx);
}
passes::compute_section_sizes(ctx);
passes::sort_output_sections(ctx);
passes::create_output_symtab(ctx);
+1 -1
View File
@@ -1135,7 +1135,7 @@ impl Symbol {
#[inline]
pub fn plt_addr<E: Target>(&self, ctx: &Context<E>) -> u64 {
if let Some(idx) = self.plt_idx(&ctx.symbols) {
return ctx.plt.hdr.shdr.sh_addr.get() + crate::chunks::plt::entry_offset::<E>(idx);
return ctx.plt.hdr.shdr.sh_addr.get() + crate::chunks::plt::entry_offset(ctx, idx);
}
ctx.pltgot.hdr.shdr.sh_addr.get()
+ self.pltgot_idx(&ctx.symbols).unwrap() as u64 * E::PLTGOT_SIZE
+48 -11
View File
@@ -20,7 +20,7 @@
//! Instructions are little-endian even on big-endian targets, where only
//! data is byte-swapped.
use crate::chunks::eh_frame;
use crate::chunks::{eh_frame, note_property, plt};
use crate::context::Context;
use crate::elf::*;
use crate::input_sections::NonAllocReloc;
@@ -94,8 +94,31 @@ fn is_add(loc: &[u8]) -> bool {
}
const NOP: u32 = 0xd503_201f;
const BTI_C: u32 = 0xd503_245f;
/// Writes a `bti c` to the beginning of `buf` if `needed` is true. Returns
/// the offset of the next instruction.
fn write_landing_pad(buf: &mut [u8], needed: bool) -> usize {
if needed {
write_insn(buf, BTI_C);
4
} else {
0
}
}
impl<const LE: bool> Arm64Target<LE> {
/// Whether a symbol's PLT entry needs a BTI landing pad. A PLT entry is
/// usually reached only by direct calls, but its address is used as the
/// function's address if the PLT is canonical or belongs to an ifunc,
/// and a range extension thunk jumps to it with an indirect branch.
fn plt_needs_landing_pad(ctx: &Context<Self>, sym: &Symbol) -> bool {
note_property::is_bti(ctx)
&& (sym.is_canonical()
|| sym.is_ifunc()
|| sym.aux(&ctx.symbols).is_some_and(|aux| !aux.thunk_addrs.is_empty()))
}
/// Whether the ADRP+ADD pair at relocation `i` can become NOP+ADR,
/// which the psABI allows when the target is within ±1 MiB.
fn relaxes_adrp_add(
@@ -187,28 +210,39 @@ impl<const LE: bool> Target for Arm64Target<LE> {
0xd420_7d00, // brk
0xd420_7d00, // brk
];
for (i, &v) in INSN.iter().enumerate() {
write_insn(&mut buf[i * 4..], v);
// PLT entries jump to the header with an indirect branch. A landing
// pad, if any, replaces one of the trailing brks.
let off = write_landing_pad(buf, note_property::is_bti(ctx));
let buf = &mut buf[off..Self::PLT_HDR_SIZE as usize];
for (loc, &v) in buf.chunks_exact_mut(4).zip(&INSN) {
write_insn(loc, v);
}
let gotplt = ctx.gotplt.shdr.sh_addr.get() + 16;
let plt = ctx.plt.hdr.shdr.sh_addr.get();
let plt = ctx.plt.hdr.shdr.sh_addr.get() + off as u64;
write_adrp(&mut buf[4..], page(gotplt).wrapping_sub(page(plt + 4)));
or_insn(&mut buf[8..], (bits(gotplt, 11, 3) << 10) as u32);
or_insn(&mut buf[12..], ((gotplt & 0xfff) << 10) as u32);
}
// A PLT entry is 16 bytes long by default and 24 bytes long if BTI is
// enabled. The extra space holds a `bti c` if the entry needs one, and
// the rest of the entry is filled with brks.
fn write_plt_entry(ctx: &Context<Self>, buf: &mut [u8], sym: &Symbol) {
const INSN: [u32; 4] = [
const INSN: [u32; 6] = [
0x9000_0010, // adrp x16, .got.plt[n]
0xf940_0211, // ldr x17, [x16, .got.plt[n]]
0x9100_0210, // add x16, x16, .got.plt[n]
0xd61f_0220, // br x17
0xd420_7d00, // brk
0xd420_7d00, // brk
];
for (i, &v) in INSN.iter().enumerate() {
write_insn(&mut buf[i * 4..], v);
let off = write_landing_pad(buf, Self::plt_needs_landing_pad(ctx, sym));
let buf = &mut buf[off..plt::entry_size(ctx) as usize];
for (loc, &v) in buf.chunks_exact_mut(4).zip(&INSN) {
write_insn(loc, v);
}
let gotplt = sym.gotplt_addr(ctx);
let plt = sym.plt_addr(ctx);
let plt = sym.plt_addr(ctx) + off as u64;
write_adrp(buf, page(gotplt).wrapping_sub(page(plt)));
or_insn(&mut buf[4..], (bits(gotplt, 11, 3) << 10) as u32);
or_insn(&mut buf[8..], ((gotplt & 0xfff) << 10) as u32);
@@ -221,11 +255,14 @@ impl<const LE: bool> Target for Arm64Target<LE> {
0xd61f_0220, // br x17
0xd420_7d00, // brk
];
for (i, &v) in INSN.iter().enumerate() {
write_insn(&mut buf[i * 4..], v);
// A landing pad, if any, replaces the trailing brk.
let off = write_landing_pad(buf, Self::plt_needs_landing_pad(ctx, sym));
let buf = &mut buf[off..Self::PLTGOT_SIZE as usize];
for (loc, &v) in buf.chunks_exact_mut(4).zip(&INSN) {
write_insn(loc, v);
}
let got = sym.got_pltgot_addr(ctx);
let plt = sym.plt_addr(ctx);
let plt = sym.plt_addr(ctx) + off as u64;
write_adrp(buf, page(got).wrapping_sub(page(plt)));
or_insn(&mut buf[4..], (bits(got, 11, 3) << 10) as u32);
}
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
. $(dirname $0)/common.inc
test_cflags -mbranch-protection=bti || skip
cat <<EOF | $CC -fPIC -mbranch-protection=bti -c -o $t/a.o -xc -
int foo() { return 1; }
int bar() { return 2; }
int baz() { return 3; }
int call(int (*fn)()) { return fn(); }
EOF
$CC -B. -shared -nostdlib -o $t/b.so $t/a.o
# All PLT entries jump to the PLT header indirectly. Other than that, a
# PLT entry needs a landing pad only if it's reached by a range extension
# thunk (foo and call), if it's a canonical PLT (baz) or if it belongs to
# an ifunc (qux).
cat <<EOF | $CC -fno-PIC -mbranch-protection=bti -c -o $t/c.o -xc -
int foo();
int bar();
int baz();
int call(int (*fn)());
static int impl() { return 4; }
static void *resolve() { return impl; }
int qux() __attribute__((ifunc("resolve")));
__attribute__((section(".low"))) int low() { return bar(); }
void _start() {
register long x0 __asm__("x0") = foo() + low() + call(baz) + call(qux) == 10 ? 0 : 1;
register long x8 __asm__("x8") = 93; // exit
__asm__ volatile("svc 0" :: "r"(x0), "r"(x8));
}
EOF
$CC -B. -no-pie -nostdlib -o $t/exe $t/c.o $t/b.so \
-Wl,--section-start=.low=0x10000000,--section-start=.text=0x20000000
readelf -n $t/exe | grep 'AArch64 feature: BTI'
readelf --dynamic $t/exe | grep AARCH64_BTI_PLT
$OBJDUMP -d -j .plt -j .plt.got $t/exe > $t/log
grep -A1 '<_PROCEDURE_LINKAGE_TABLE_>:' $t/log | grep -w bti
grep -A1 -E '<foo[@$]plt>:' $t/log | grep -w bti
grep -A1 -E '<bar[@$]plt>:' $t/log | not grep -w bti
grep -A1 -E '<baz[@$]plt>:' $t/log | grep -w bti
grep -A1 -E '<call[@$]plt>:' $t/log | grep -w bti
grep -A1 '<qux$pltgot>:' $t/log | grep -w bti
$QEMU $t/exe
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
. $(dirname $0)/common.inc
test_cflags -mbranch-protection=standard || skip
cat <<EOF | $CC -mbranch-protection=standard -c -o $t/a.o -xc -
void _start() {}
EOF
cat <<EOF | $CC -mbranch-protection=standard -c -o $t/b.o -xc -
void foo() {}
EOF
cat <<EOF | $CC -mbranch-protection=bti -c -o $t/c.o -xc -
void bar() {}
EOF
cat <<EOF | $CC -mbranch-protection=none -c -o $t/d.o -xc -
void baz() {}
EOF
./mold -o $t/exe1 $t/a.o $t/b.o
readelf -n $t/exe1 | grep 'AArch64 feature: BTI, PAC'
readelf -W --segments $t/exe1 | grep -w GNU_PROPERTY
./mold -o $t/exe2 $t/a.o $t/b.o $t/c.o
readelf -n $t/exe2 | grep 'AArch64 feature: BTI$'
./mold -o $t/exe3 $t/a.o $t/b.o $t/c.o $t/d.o
readelf -n $t/exe3 | not grep 'AArch64 feature'
./mold -r -o $t/e.o $t/a.o $t/b.o
readelf -n $t/e.o | grep 'AArch64 feature: BTI, PAC'