Skip to content

Commit 52e4faf

Browse files
committed
Fixes
1 parent 250b8bc commit 52e4faf

File tree

3 files changed

+120
-63
lines changed

3 files changed

+120
-63
lines changed

ci/run.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ fi
5656
symcheck=(cargo run -p symbol-check --release)
5757
[[ "$target" = "wasm"* ]] && symcheck+=(--features wasm)
5858
symcheck+=(-- build-and-check)
59+
[[ "$target" = *"thumb"* ]] && symcheck+=(--no-std)
5960

6061
"${symcheck[@]}" "$target" -- -p compiler_builtins
6162
"${symcheck[@]}" "$target" -- -p compiler_builtins --release

crates/symbol-check/src/main.rs

Lines changed: 117 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22
//! linking errors.
33
44
use std::collections::{BTreeMap, BTreeSet};
5-
use std::fs;
65
use std::io::{BufRead, BufReader};
76
use std::path::{Path, PathBuf};
87
use std::process::{Command, Stdio};
8+
use std::{env, fs};
99

1010
use object::read::archive::ArchiveFile;
1111
use object::{
12-
BinaryFormat, File as ObjFile, Object, ObjectSection, ObjectSymbol, Result as ObjResult,
13-
SectionFlags, Symbol, SymbolKind, SymbolScope, elf,
12+
Architecture, BinaryFormat, Bytes, Endianness, File as ObjFile, Object, ObjectSection,
13+
ObjectSymbol, Result as ObjResult, SectionFlags, SectionKind, Symbol, SymbolKind, SymbolScope,
14+
elf,
1415
};
1516
use serde_json::Value;
1617

@@ -19,65 +20,78 @@ const CHECK_EXTENSIONS: &[Option<&str>] = &[Some("rlib"), Some("a"), Some("exe")
1920

2021
const USAGE: &str = "Usage:
2122
22-
symbol-check build-and-check [TARGET] -- CARGO_BUILD_ARGS ...
23+
symbol-check build-and-check [TARGET] [--no-std] -- CARGO_BUILD_ARGS ...
2324
2425
Cargo will get invoked with `CARGO_ARGS` and the specified target. All output
2526
`compiler_builtins*.rlib` files will be checked.
2627
2728
If TARGET is not specified, the host target is used.
2829
29-
check PATHS ...
30+
If the `--no-std` flag is passed, the binaries will not be checked for
31+
executable stacks under the assumption that they are not being emitted.
32+
33+
check [--no-std] PATHS ...
3034
3135
Run the same checks on the given set of paths, without invoking Cargo. Paths
3236
may be either archives or object files.
3337
";
3438

35-
fn main() {
36-
// Create a `&str` vec so we can match on it.
37-
let args = std::env::args().collect::<Vec<_>>();
38-
let args_ref = args.iter().map(String::as_str).collect::<Vec<_>>();
39+
#[derive(Debug, PartialEq)]
40+
enum Mode {
41+
BuildAndCheck,
42+
CheckOnly,
43+
}
3944

40-
match &args_ref[1..] {
41-
["build-and-check", target, "--", args @ ..] if !args.is_empty() => {
42-
run_build_and_check(target, args);
43-
}
44-
["build-and-check", "--", args @ ..] if !args.is_empty() => {
45-
let target = &host_target();
46-
run_build_and_check(target, args);
47-
}
48-
["check", paths @ ..] if !paths.is_empty() => {
49-
check_paths(paths);
50-
}
51-
_ => {
52-
println!("{USAGE}");
53-
std::process::exit(1);
45+
fn main() {
46+
let mut args_iter = env::args().skip(1);
47+
let mode = match args_iter.next() {
48+
Some(arg) if arg == "build-and-check" => Mode::BuildAndCheck,
49+
Some(arg) if arg == "check" => Mode::CheckOnly,
50+
Some(other) => invalid_usage(&format!("unrecognized mode `{other}`")),
51+
None => invalid_usage("mode must be specified"),
52+
};
53+
54+
let mut target = None;
55+
let mut verify_no_exe = true;
56+
57+
for arg in args_iter.by_ref() {
58+
match arg.as_str() {
59+
"--no-std" => verify_no_exe = false,
60+
"--" => break,
61+
f if f.starts_with("-") => invalid_usage(&format!("unrecognized flag `{f}`")),
62+
_ if mode == Mode::BuildAndCheck => target = Some(arg),
63+
_ => break,
5464
}
5565
}
56-
}
5766

58-
fn run_build_and_check(target: &str, args: &[&str]) {
59-
// Make sure `--target` isn't passed to avoid confusion (since it should be
60-
// proivded only once, positionally).
61-
for arg in args {
62-
assert!(
63-
!arg.contains("--target"),
64-
"target must be passed positionally. {USAGE}"
65-
);
66-
}
67+
let positional = args_iter.collect::<Vec<_>>();
6768

68-
let paths = exec_cargo_with_args(target, args);
69-
check_paths(&paths);
69+
match mode {
70+
Mode::BuildAndCheck => {
71+
let target = target.unwrap_or_else(|| host_target());
72+
let paths = exec_cargo_with_args(&target, positional.as_slice());
73+
check_paths(&paths, verify_no_exe);
74+
}
75+
Mode::CheckOnly => check_paths(&positional, verify_no_exe),
76+
};
7077
}
7178

72-
fn check_paths<P: AsRef<Path>>(paths: &[P]) {
79+
fn invalid_usage(s: &str) -> ! {
80+
println!("{s}\n{USAGE}");
81+
std::process::exit(1);
82+
}
83+
84+
fn check_paths<P: AsRef<Path>>(paths: &[P], verify_no_exe: bool) {
7385
for path in paths {
7486
let path = path.as_ref();
7587
println!("Checking {}", path.display());
7688
let archive = BinFile::from_path(path);
7789

78-
verify_no_duplicates(&archive);
79-
verify_core_symbols(&archive);
80-
verify_no_exec_stack(&archive);
90+
// verify_no_duplicates(&archive);
91+
// verify_core_symbols(&archive);
92+
if verify_no_exe {
93+
verify_no_exec_stack(&archive);
94+
}
8195
}
8296
}
8397

@@ -97,15 +111,15 @@ fn host_target() -> String {
97111

98112
/// Run `cargo build` with the provided additional arguments, collecting the list of created
99113
/// libraries.
100-
fn exec_cargo_with_args(target: &str, args: &[&str]) -> Vec<PathBuf> {
114+
fn exec_cargo_with_args<S: AsRef<str>>(target: &str, args: &[S]) -> Vec<PathBuf> {
101115
let mut cmd = Command::new("cargo");
102116
cmd.args([
103117
"build",
104118
"--target",
105119
target,
106120
"--message-format=json-diagnostic-rendered-ansi",
107121
])
108-
.args(args)
122+
.args(args.iter().map(|arg| arg.as_ref()))
109123
.stdout(Stdio::piped());
110124

111125
println!("running: {cmd:?}");
@@ -300,18 +314,7 @@ fn verify_core_symbols(archive: &BinFile) {
300314
println!(" success: no undefined references to core found");
301315
}
302316

303-
/// Check that all object files contain a section named `.note.GNU-stack`, indicating a
304-
/// nonexecutable stack.
305-
///
306-
/// Paraphrased from <https://www.man7.org/linux/man-pages/man1/ld.1.html>:
307-
///
308-
/// - A `.note.GNU-stack` section with the exe flag means this needs an executable stack
309-
/// - A `.note.GNU-stack` section without the exe flag means there is no executable stack needed
310-
/// - Without the section, behavior is target-specific and on some targets means an executable
311-
/// stack is required.
312-
///
313-
/// Now says
314-
/// deprecated <https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=0d38576a34ec64a1b4500c9277a8e9d0f07e6774>.
317+
/// Ensure that the object/archive will not require an executable stack.
315318
fn verify_no_exec_stack(archive: &BinFile) {
316319
let mut problem_objfiles = Vec::new();
317320

@@ -322,42 +325,91 @@ fn verify_no_exec_stack(archive: &BinFile) {
322325
});
323326

324327
if !problem_objfiles.is_empty() {
325-
panic!("the following archive members require an executable stack: {problem_objfiles:#?}");
328+
panic!("the following object files require an executable stack: {problem_objfiles:#?}");
326329
}
327330

328-
println!(" success: no writeable-executable sections found");
331+
println!(" success: no writeable+executable sections found");
329332
}
330333

334+
/// True if the section/flag combination indicates that the object file should be linked with an
335+
/// executable stack.
336+
///
337+
/// Paraphrased from <https://www.man7.org/linux/man-pages/man1/ld.1.html>:
338+
///
339+
/// - A `.note.GNU-stack` section with the exe flag means this needs an executable stack
340+
/// - A `.note.GNU-stack` section without the exe flag means there is no executable stack needed
341+
/// - Without the section, behavior is target-specific and on some targets means an executable
342+
/// stack is required.
343+
///
344+
/// If any object files meet the requirements for an executable stack, any final binary that links
345+
/// it will have a program header with a `PT_GNU_STACK` section, which will be marked `RWE` rather
346+
/// than the desired `RW`. (We don't check final binaries).
347+
///
348+
/// Per [1], it is now deprecated behavior for a missing `.note.GNU-stack` section to imply an
349+
/// executable stack. However, we shouldn't assume that tooling has caught up to this.
350+
///
351+
/// [1]: https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=0d38576a34ec64a1b4500c9277a8e9d0f07e6774>
331352
fn obj_requires_exe_stack(obj: &ObjFile) -> bool {
332-
// Files other than elf likely do not use the same convention.
353+
// Files other than elf do not use the same convention.
333354
if obj.format() != BinaryFormat::Elf {
334355
return false;
335356
}
336357

358+
let mut return_immediate = None;
337359
let mut has_exe_sections = false;
338360
for sec in obj.sections() {
339361
let SectionFlags::Elf { sh_flags } = sec.flags() else {
340362
unreachable!("only elf files are being checked");
341363
};
342364

343-
let exe = (sh_flags & elf::SHF_EXECINSTR as u64) != 0;
365+
if sec.kind() == SectionKind::Elf(elf::SHT_ARM_ATTRIBUTES) {
366+
let data = sec.data().unwrap();
367+
parse_arm_thing(data);
368+
}
369+
370+
let is_exe = (sh_flags & elf::SHF_EXECINSTR as u64) != 0;
344371

345372
// If the magic section is present, its exe bit tells us whether or not the object
346373
// file requires an executable stack.
347374
if sec.name().unwrap_or_default() == ".note.GNU-stack" {
348-
return exe;
375+
return_immediate = Some(is_exe);
349376
}
350377

351378
// Otherwise, just keep track of whether or not we have exeuctable sections
352-
has_exe_sections |= exe;
379+
has_exe_sections |= is_exe;
380+
}
381+
382+
if let Some(imm) = return_immediate {
383+
return imm;
353384
}
354385

355386
// Ignore object files that have no executable sections, like rmeta
356387
if !has_exe_sections {
357388
return false;
358389
}
359390

360-
true
391+
platform_default_exe_stack_required(obj.architecture(), obj.endianness())
392+
}
393+
394+
/// Default if there is no `.note.GNU-stack` section.
395+
fn platform_default_exe_stack_required(arch: Architecture, end: Endianness) -> bool {
396+
match arch {
397+
// PPC64 doesn't set `.note.GNU-stack` since GNU nested functions don't need a trampoline,
398+
// <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=21098>.
399+
Architecture::PowerPc64 if end == Endianness::Big => false,
400+
_ => true,
401+
}
402+
}
403+
404+
fn parse_arm_thing(data: &[u8]) {
405+
eprintln!("data: {data:x?}");
406+
eprintln!("data string: {:?}", String::from_utf8_lossy(data));
407+
eprintln!("data: {:x?}", &data[16..]);
408+
// let mut rest = &data[16..];
409+
let mut b = Bytes(data);
410+
b.skip(16).unwrap();
411+
412+
// while !rest.is_empty() {}
361413
}
362414

363415
/// Thin wrapper for owning data used by `object`.
@@ -448,8 +500,12 @@ fn check_no_gnu_stack_obj() {
448500
}
449501

450502
#[test]
451-
#[cfg_attr(not(target_env = "gnu"), ignore = "requires a gnu toolchain to build")]
503+
#[cfg_attr(
504+
any(target_os = "windows", target_vendor = "apple"),
505+
ignore = "requires elf format"
506+
)]
452507
fn check_obj() {
508+
#[expect(clippy::option_env_unwrap, reason = "test is ignored")]
453509
let p = option_env!("HAS_EXE_STACK_OBJ").expect("has_exe_stack.o not present");
454510
let f = fs::read(p).unwrap();
455511
let obj = ObjFile::parse(f.as_slice()).unwrap();

crates/symbol-check/tests/has_exe_stack.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
/* GNU nested functions are the only way I could fine to force an executable
2-
* stack. Supported by GCC only, not Clang. */
1+
/* GNU nested functions are the only way I could find to force an explicitly
2+
* executable stack. Supported by GCC only, not Clang. */
33

44
void intermediate(void (*)(int, int), int);
55

0 commit comments

Comments
 (0)