Support -z force-bti, pac-plt, gcs, bti-report and gcs-report

-z force-bti and -z pac-plt force the BTI and PAC bits of the output's
GNU_PROPERTY_AARCH64_FEATURE_1_AND, and -z pac-plt also makes PLT entries
authenticate .got.plt entries with autia1716 and sets DT_AARCH64_PAC_PLT.
-z gcs=implicit|never|always controls the GCS bit. -z bti-report and
-z gcs-report diagnose input files lacking the features, and as in lld,
-z force-bti and -z gcs=always imply the warning level.
This commit is contained in:
Rui Ueyama committed 2026-10-01 10:09:13 +09:00
1 parent f03b745952
commit 48428d3890
12 files changed
+306 -15

No files matched your search

+36
View File
@@ -847,6 +847,42 @@ point symbol in a script.
`GNU_PROPERTY_X86_FEATURE_1_SHSTK` bit in `.note.gnu.property` section to
determine whether or not an object file was compiled with `-fcf-protection`.
* `-z bti-report`=[ `warning` | `error` | `none` ]:
Branch Target Identification (BTI) is an ARM64 feature to protect programs
from control hijacking attacks by requiring indirect branches to land on
`bti` instructions. You can tell the compiler to use the feature by
specifying the `-mbranch-protection=bti` or `-mbranch-protection=standard`
flag. An output file is marked as BTI-enabled only if all object files are.
`-z bti-report` flag is used to make sure that all object files were
compiled with BTI. If `warning` or `error` are given, `mold` prints out a
warning or an error message if an object file lacks
`GNU_PROPERTY_AARCH64_FEATURE_1_BTI` bit in its `.note.gnu.property`
section.
* `-z force-bti`:
Turn on `GNU_PROPERTY_AARCH64_FEATURE_1_BTI` bit in `.note.gnu.property`
section even if some object files were not compiled with BTI, and generate
a PLT with BTI landing pads. This option implies `-z bti-report=warning`.
* `-z gcs`=[ `implicit` | `always` | `never` ]:
Guarded Control Stack (GCS) is an ARM64 shadow stack feature. With
`implicit`, which is the default, `GNU_PROPERTY_AARCH64_FEATURE_1_GCS` bit
in `.note.gnu.property` section is turned on only if all object files have
the bit. `always` turns it on regardless of object files and implies `-z
gcs-report=warning`. `never` turns it off.
* `-z gcs-report`=[ `warning` | `error` | `none` ]:
Print out a warning or an error message if an object file lacks
`GNU_PROPERTY_AARCH64_FEATURE_1_GCS` bit in its `.note.gnu.property`
section.
* `-z pac-plt`:
Generate an ARM64 PLT that authenticates function addresses read from
`.got.plt` using Pointer Authentication (PAC), and turn on
`GNU_PROPERTY_AARCH64_FEATURE_1_PAC` bit in `.note.gnu.property` section.
The output works only with a dynamic linker that signs `.got.plt` entries.
* `-z now`, `-z lazy`:
By default, functions referring to other ELF modules are resolved by the
dynamic linker when they are called for the first time. `-z now` marks an
+3
View File
@@ -233,6 +233,9 @@ fn for_each_entry<E: Target>(ctx: &Context<E>, mut define: impl FnMut(u32, u64))
if note_property::is_bti(ctx) {
define(DT_AARCH64_BTI_PLT, 0);
}
if E::FAMILY == Family::Arm64 && ctx.args.z_pac_plt {
define(DT_AARCH64_PAC_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);
+16
View File
@@ -3,6 +3,7 @@
use std::collections::{BTreeMap, BTreeSet};
use crate::chunks::ChunkHeader;
use crate::cmdline::GcsKind;
use crate::context::Context;
use crate::elf::*;
use crate::target::{Family, Target};
@@ -101,6 +102,21 @@ pub fn construct<E: Target>(ctx: &mut Context<E>) {
*map.entry(GNU_PROPERTY_X86_ISA_1_NEEDED).or_insert(0) |= ctx.args.z_x86_64_isa_level;
}
if E::FAMILY == Family::Arm64 {
let features = map.entry(GNU_PROPERTY_AARCH64_FEATURE_1_AND).or_insert(0);
if ctx.args.z_force_bti {
*features |= GNU_PROPERTY_AARCH64_FEATURE_1_BTI;
}
if ctx.args.z_pac_plt {
*features |= GNU_PROPERTY_AARCH64_FEATURE_1_PAC;
}
match ctx.args.z_gcs {
GcsKind::Implicit => {}
GcsKind::Never => *features &= !GNU_PROPERTY_AARCH64_FEATURE_1_GCS,
GcsKind::Always => *features |= GNU_PROPERTY_AARCH64_FEATURE_1_GCS,
}
}
ctx.note_property.as_mut().unwrap().contents =
map.into_iter().filter(|&(_, v)| v != 0).collect();
}
+7 -2
View File
@@ -47,8 +47,13 @@ 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 }
// landing pad if BTI is enabled and for an `autia1716` if -z pac-plt is
// given.
if E::FAMILY == Family::Arm64 && (note_property::is_bti(ctx) || ctx.args.z_pac_plt) {
24
} else {
E::PLT_SIZE
}
}
/// The offset of a PLT entry within `.plt`.
+58 -6
View File
@@ -283,13 +283,21 @@ pub enum SeparateCodeKind {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum CetReportKind {
pub enum ReportKind {
#[default]
None,
Warning,
Error,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum GcsKind {
#[default]
Implicit,
Never,
Always,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ShuffleSections {
#[default]
@@ -426,7 +434,10 @@ pub struct ReaderJob {
pub struct Args {
pub bsymbolic: BsymbolicKind,
pub build_id: BuildId,
pub z_cet_report: CetReportKind,
pub z_cet_report: ReportKind,
pub z_bti_report: ReportKind,
pub z_gcs_report: ReportKind,
pub z_gcs: GcsKind,
pub undefined_glob: Glob,
pub unique: Glob,
pub z_separate_code: SeparateCodeKind,
@@ -490,6 +501,7 @@ pub struct Args {
pub z_dynamic_undefined_weak: bool,
pub z_execstack: bool,
pub z_execstack_if_needed: bool,
pub z_force_bti: bool,
pub z_ibt: bool,
pub z_initfirst: bool,
pub z_interpose: bool,
@@ -497,6 +509,7 @@ pub struct Args {
pub z_nodefaultlib: bool,
pub z_now: bool,
pub z_origin: bool,
pub z_pac_plt: bool,
pub z_relro: bool,
pub z_rewrite_endbr: bool,
pub z_rodynamic: bool,
@@ -563,7 +576,10 @@ impl Default for Args {
Self {
bsymbolic: BsymbolicKind::None,
build_id: BuildId::default(),
z_cet_report: CetReportKind::None,
z_cet_report: ReportKind::None,
z_bti_report: ReportKind::None,
z_gcs_report: ReportKind::None,
z_gcs: GcsKind::Implicit,
undefined_glob: Glob::new(),
unique: Glob::new(),
z_separate_code: SeparateCodeKind::NoSeparateCode,
@@ -627,6 +643,7 @@ impl Default for Args {
z_dynamic_undefined_weak: true,
z_execstack: false,
z_execstack_if_needed: false,
z_force_bti: false,
z_ibt: false,
z_initfirst: false,
z_interpose: false,
@@ -634,6 +651,7 @@ impl Default for Args {
z_nodefaultlib: false,
z_now: false,
z_origin: false,
z_pac_plt: false,
z_relro: true,
z_rewrite_endbr: false,
z_rodynamic: false,
@@ -1214,6 +1232,8 @@ pub fn parse_args(target: &TargetTraits, raw_cmdline: &[Cow<'_, OsStr>]) -> Pars
let mut report_undefined: Option<bool> = None;
let mut z_relro: Option<bool> = None;
let mut z_dynamic_undefined_weak: Option<bool> = None;
let mut z_bti_report: Option<ReportKind> = None;
let mut z_gcs_report: Option<ReportKind> = None;
let mut separate_debug_file: Option<PathBuf> = None;
// An explicit seed survives intervening --reverse-sections options.
let mut shuffle_sections_seed: Option<u64> = None;
@@ -1615,11 +1635,23 @@ pub fn parse_args(target: &TargetTraits, raw_cmdline: &[Cow<'_, OsStr>]) -> Pars
} else if let Some(value) = cursor.read_z_switch("now", "lazy") {
a.z_now = value;
} else if cursor.read_z_flag("cet-report=none") {
a.z_cet_report = CetReportKind::None;
a.z_cet_report = ReportKind::None;
} else if cursor.read_z_flag("cet-report=warning") {
a.z_cet_report = CetReportKind::Warning;
a.z_cet_report = ReportKind::Warning;
} else if cursor.read_z_flag("cet-report=error") {
a.z_cet_report = CetReportKind::Error;
a.z_cet_report = ReportKind::Error;
} else if cursor.read_z_flag("bti-report=none") {
z_bti_report = Some(ReportKind::None);
} else if cursor.read_z_flag("bti-report=warning") {
z_bti_report = Some(ReportKind::Warning);
} else if cursor.read_z_flag("bti-report=error") {
z_bti_report = Some(ReportKind::Error);
} else if cursor.read_z_flag("gcs-report=none") {
z_gcs_report = Some(ReportKind::None);
} else if cursor.read_z_flag("gcs-report=warning") {
z_gcs_report = Some(ReportKind::Warning);
} else if cursor.read_z_flag("gcs-report=error") {
z_gcs_report = Some(ReportKind::Error);
} else if cursor.read_z_flag("execstack") {
a.z_execstack = true;
} else if cursor.read_z_flag("execstack-if-needed") {
@@ -1658,6 +1690,16 @@ pub fn parse_args(target: &TargetTraits, raw_cmdline: &[Cow<'_, OsStr>]) -> Pars
} else if cursor.read_z_flag("ibt") {
a.z_ibt = true;
} else if cursor.read_z_flag("ibtplt") {
} else if cursor.read_z_flag("force-bti") {
a.z_force_bti = true;
} else if cursor.read_z_flag("pac-plt") {
a.z_pac_plt = true;
} else if cursor.read_z_flag("gcs=implicit") {
a.z_gcs = GcsKind::Implicit;
} else if cursor.read_z_flag("gcs=never") {
a.z_gcs = GcsKind::Never;
} else if cursor.read_z_flag("gcs=always") {
a.z_gcs = GcsKind::Always;
} else if cursor.read_z_flag("muldefs") {
a.allow_multiple_definition = true;
} else if let Some(value) =
@@ -2025,6 +2067,16 @@ pub fn parse_args(target: &TargetTraits, raw_cmdline: &[Cow<'_, OsStr>]) -> Pars
// `-z dynamic-undefined-weak` is enabled by default for DSOs.
a.z_dynamic_undefined_weak = z_dynamic_undefined_weak.unwrap_or(a.shared);
// `-z force-bti` implies `-z bti-report=warning`, and `-z gcs=always`
// implies `-z gcs-report=warning`.
a.z_bti_report =
z_bti_report.unwrap_or(if a.z_force_bti { ReportKind::Warning } else { ReportKind::None });
a.z_gcs_report = z_gcs_report.unwrap_or(if a.z_gcs == GcsKind::Always {
ReportKind::Warning
} else {
ReportKind::None
});
// --section-order implies `-z norelro`
a.z_relro = z_relro.unwrap_or(a.section_order.is_empty());
if a.nmagic || a.omagic {
+8 -1
View File
@@ -327,9 +327,16 @@ pub fn link<E: Target>(cmdline: Cmdline) -> LinkResult {
// or ctx.dsos.
// Handle `-z cet-report`.
if ctx.args.z_cet_report != cmdline::CetReportKind::None {
if ctx.args.z_cet_report != cmdline::ReportKind::None {
passes::check_cet_errors(&ctx);
}
// Handle `-z bti-report` and `-z gcs-report`.
if E::FAMILY == Family::Arm64
&& (ctx.args.z_bti_report != cmdline::ReportKind::None
|| ctx.args.z_gcs_report != cmdline::ReportKind::None)
{
passes::check_arm64_feature_errors(&ctx);
}
// Handle `-z execstack-if-needed`.
if ctx.args.z_execstack_if_needed && ctx.objs.iter().any(|f| f.needs_executable_stack) {
ctx.args.z_execstack = true;
+3
View File
@@ -220,6 +220,7 @@ 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_PAC_PLT: u32 = 0x70000003;
pub const DT_AARCH64_VARIANT_PCS: u32 = 0x70000005;
pub const DT_AUXILIARY: u32 = 0x7ffffffd;
pub const DT_FILTER: u32 = 0x7fffffff;
@@ -266,6 +267,8 @@ 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 GNU_PROPERTY_AARCH64_FEATURE_1_PAC: u32 = 2;
pub const GNU_PROPERTY_AARCH64_FEATURE_1_GCS: u32 = 4;
pub const ELFCOMPRESS_NONE: u32 = 0;
pub const ELFCOMPRESS_ZLIB: u32 = 1;
+33 -3
View File
@@ -21,8 +21,8 @@ use crate::chunks::{
self, ChunkHeader, ChunkId, OutputPhdr, OutputSectionId, compressed, copyrel, dynsym, reloc,
};
use crate::cmdline::{
BsymbolicKind, BuildId, CetReportKind, DefsymValue, ReportOutput, SectionOrder,
SeparateCodeKind, ShuffleSections, UnresolvedKind,
BsymbolicKind, BuildId, DefsymValue, ReportKind, ReportOutput, SectionOrder, SeparateCodeKind,
ShuffleSections, UnresolvedKind,
};
use crate::context::Context;
use crate::elf::*;
@@ -1510,7 +1510,7 @@ pub fn apply_section_align<E: Target>(ctx: &mut Context<E>) {
}
pub fn check_cet_errors<E: Target>(ctx: &Context<E>) {
let warning = ctx.args.z_cet_report == CetReportKind::Warning;
let warning = ctx.args.z_cet_report == ReportKind::Warning;
let has_feature = |file: &ObjectFile<E>, feature: u32| {
file.gnu_properties.get(&GNU_PROPERTY_X86_FEATURE_1_AND).is_some_and(|v| v & feature != 0)
};
@@ -1532,6 +1532,36 @@ pub fn check_cet_errors<E: Target>(ctx: &Context<E>) {
}
}
pub fn check_arm64_feature_errors<E: Target>(ctx: &Context<E>) {
let has_feature = |file: &ObjectFile<E>, feature: u32| {
file.gnu_properties
.get(&GNU_PROPERTY_AARCH64_FEATURE_1_AND)
.is_some_and(|v| v & feature != 0)
};
for file in &ctx.objs {
if ctx.is_internal(file.id()) {
continue;
}
for (kind, option, feature, name) in [
(ctx.args.z_bti_report, "bti-report", GNU_PROPERTY_AARCH64_FEATURE_1_BTI, "BTI"),
(ctx.args.z_gcs_report, "gcs-report", GNU_PROPERTY_AARCH64_FEATURE_1_GCS, "GCS"),
] {
if has_feature(file, feature) {
continue;
}
match kind {
ReportKind::None => {}
ReportKind::Warning => warn!(
"{file}: -z {option}=warning: missing GNU_PROPERTY_AARCH64_FEATURE_1_{name}"
),
ReportKind::Error => error!(
"{file}: -z {option}=error: missing GNU_PROPERTY_AARCH64_FEATURE_1_{name}"
),
}
}
}
}
pub fn print_dependencies<E: Target>(ctx: &Context<E>) {
ReportOutput::Stdout.with_writer("--print-dependencies", |out| {
writeln!(
+17 -3
View File
@@ -225,8 +225,13 @@ impl<const LE: bool> Target for Arm64Target<LE> {
}
// 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.
// enabled or -z pac-plt is given. The extra space is used for `bti c` if
// the entry needs one and for `autia1716` if -z pac-plt is given, and the
// rest is filled with brks.
//
// With -z pac-plt, the dynamic linker is expected to sign .got.plt entries
// with `pacia1716` using the entry's address as a modifier, and the PLT
// entry authenticates the address before jumping to it.
fn write_plt_entry(ctx: &Context<Self>, buf: &mut [u8], sym: &Symbol) {
const INSN: [u32; 6] = [
0x9000_0010, // adrp x16, .got.plt[n]
@@ -236,9 +241,18 @@ impl<const LE: bool> Target for Arm64Target<LE> {
0xd420_7d00, // brk
0xd420_7d00, // brk
];
const PAC_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]
0xd503_219f, // autia1716
0xd61f_0220, // br x17
0xd420_7d00, // brk
];
let insn = if ctx.args.z_pac_plt { &PAC_INSN } else { &INSN };
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) {
for (loc, &v) in buf.chunks_exact_mut(4).zip(insn) {
write_insn(loc, v);
}
let gotplt = sym.gotplt_addr(ctx);
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
. $(dirname $0)/common.inc
test_cflags -mbranch-protection=bti || skip
cat <<EOF | $CC -fPIC -mbranch-protection=none -c -o $t/a.o -xc -
void foo() {}
EOF
cat <<EOF | $CC -fPIC -mbranch-protection=bti -c -o $t/b.o -xc -
void foo();
void bar() { foo(); }
EOF
./mold -shared -o $t/c.so $t/a.o $t/b.o
readelf -n $t/c.so | not grep 'AArch64 feature'
$OBJDUMP -d -j .plt $t/c.so | grep -A1 '<_PROCEDURE_LINKAGE_TABLE_>:' | not grep -w bti
./mold -shared -o $t/c.so $t/a.o $t/b.o -z force-bti 2> $t/log
readelf -n $t/c.so | grep 'AArch64 feature: BTI'
readelf --dynamic $t/c.so | grep AARCH64_BTI_PLT
$OBJDUMP -d -j .plt $t/c.so | grep -A1 '<_PROCEDURE_LINKAGE_TABLE_>:' | grep -w bti
grep 'a.o: -z bti-report=warning: missing GNU_PROPERTY_AARCH64_FEATURE_1_BTI' $t/log
not grep b.o: $t/log
./mold -shared -o $t/c.so $t/a.o $t/b.o -z force-bti -z bti-report=none 2> $t/log
not grep a.o: $t/log
./mold -shared -o $t/c.so $t/a.o $t/b.o -z bti-report=warning 2> $t/log
readelf -n $t/c.so | not grep 'AArch64 feature'
grep 'a.o: -z bti-report=warning: missing GNU_PROPERTY_AARCH64_FEATURE_1_BTI' $t/log
not ./mold -shared -o $t/c.so $t/a.o $t/b.o -z bti-report=error 2> $t/log
grep 'a.o: -z bti-report=error: missing GNU_PROPERTY_AARCH64_FEATURE_1_BTI' $t/log
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
. $(dirname $0)/common.inc
# a.o is marked with BTI and GCS, and b.o only with BTI.
cat <<EOF | $CC -c -o $t/a.o -xassembler-with-cpp -
#define NT_GNU_PROPERTY_TYPE_0 5
#define GNU_PROPERTY_AARCH64_FEATURE_1_AND 0xc0000000
.globl _start
_start:
ret
.section .note.gnu.property, "a"
.p2align 3
.long 4
.long 1f - 0f
.long NT_GNU_PROPERTY_TYPE_0
.asciz "GNU"
0:.long GNU_PROPERTY_AARCH64_FEATURE_1_AND
.long 4
.long 5
.p2align 3
1:
EOF
cat <<EOF | $CC -c -o $t/b.o -xassembler-with-cpp -
#define NT_GNU_PROPERTY_TYPE_0 5
#define GNU_PROPERTY_AARCH64_FEATURE_1_AND 0xc0000000
.globl foo
foo:
ret
.section .note.gnu.property, "a"
.p2align 3
.long 4
.long 1f - 0f
.long NT_GNU_PROPERTY_TYPE_0
.asciz "GNU"
0:.long GNU_PROPERTY_AARCH64_FEATURE_1_AND
.long 4
.long 1
.p2align 3
1:
EOF
# Older readelf doesn't know the name of the GCS bit.
./mold -o $t/exe1 $t/a.o
readelf -n $t/exe1 | grep -E 'AArch64 feature: BTI, (GCS|<unknown: 4>)$'
./mold -o $t/exe2 $t/a.o -z gcs=never
readelf -n $t/exe2 | grep 'AArch64 feature: BTI$'
./mold -o $t/exe3 $t/a.o $t/b.o 2> $t/log
readelf -n $t/exe3 | grep 'AArch64 feature: BTI$'
not grep . $t/log
./mold -o $t/exe4 $t/a.o $t/b.o -z gcs=always 2> $t/log
readelf -n $t/exe4 | grep -E 'AArch64 feature: BTI, (GCS|<unknown: 4>)$'
grep 'b.o: -z gcs-report=warning: missing GNU_PROPERTY_AARCH64_FEATURE_1_GCS' $t/log
not grep a.o: $t/log
./mold -o $t/exe5 $t/a.o $t/b.o -z gcs=always -z gcs-report=none 2> $t/log
not grep . $t/log
not ./mold -o $t/exe6 $t/a.o $t/b.o -z gcs-report=error 2> $t/log
grep 'b.o: -z gcs-report=error: missing GNU_PROPERTY_AARCH64_FEATURE_1_GCS' $t/log
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
. $(dirname $0)/common.inc
cat <<EOF | $CC -fPIC -c -o $t/a.o -xc -
void foo();
void bar() { foo(); }
EOF
./mold -shared -o $t/b.so $t/a.o -z pac-plt
readelf -n $t/b.so | grep 'AArch64 feature: PAC'
readelf --dynamic $t/b.so | grep AARCH64_PAC_PLT
readelf --dynamic $t/b.so | not grep AARCH64_BTI_PLT
# A PLT entry authenticates the address in x17 before jumping to it.
$OBJDUMP -d -j .plt $t/b.so | grep -A5 -E '<foo[@$]plt>:' > $t/log
grep -A1 -w autia1716 $t/log | grep 'br.*x17'
not grep -w bti $t/log
./mold -shared -o $t/b.so $t/a.o -z pac-plt -z force-bti
readelf -n $t/b.so | grep 'AArch64 feature: BTI, PAC'
readelf --dynamic $t/b.so | grep AARCH64_PAC_PLT
readelf --dynamic $t/b.so | grep AARCH64_BTI_PLT
$OBJDUMP -d -j .plt $t/b.so | grep -A5 -E '<foo[@$]plt>:' > $t/log
grep -A1 -w autia1716 $t/log | grep 'br.*x17'