Skip to content

Commit a22be63

Browse files
committed
Fixes
1 parent 250b8bc commit a22be63

File tree

3 files changed

+124
-63
lines changed

3 files changed

+124
-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: 121 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
//! Tool used by CI to inspect compiler-builtins archives and help ensure we won't run into any
22
//! linking errors.
33
4+
#![allow(unused)] // TODO
5+
46
use std::collections::{BTreeMap, BTreeSet};
5-
use std::fs;
67
use std::io::{BufRead, BufReader};
78
use std::path::{Path, PathBuf};
89
use std::process::{Command, Stdio};
10+
use std::{env, fs};
911

1012
use object::read::archive::ArchiveFile;
1113
use object::{
12-
BinaryFormat, File as ObjFile, Object, ObjectSection, ObjectSymbol, Result as ObjResult,
13-
SectionFlags, Symbol, SymbolKind, SymbolScope, elf,
14+
Architecture, BinaryFormat, Bytes, Endianness, File as ObjFile, Object, ObjectSection,
15+
ObjectSymbol, Result as ObjResult, SectionFlags, SectionKind, Symbol, SymbolKind, SymbolScope,
16+
elf,
1417
};
1518
use serde_json::Value;
1619

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

2023
const USAGE: &str = "Usage:
2124
22-
symbol-check build-and-check [TARGET] -- CARGO_BUILD_ARGS ...
25+
symbol-check build-and-check [TARGET] [--no-std] -- CARGO_BUILD_ARGS ...
2326
2427
Cargo will get invoked with `CARGO_ARGS` and the specified target. All output
2528
`compiler_builtins*.rlib` files will be checked.
2629
2730
If TARGET is not specified, the host target is used.
2831
29-
check PATHS ...
32+
If the `--no-std` flag is passed, the binaries will not be checked for
33+
executable stacks under the assumption that they are not being emitted.
34+
35+
check [--no-std] PATHS ...
3036
3137
Run the same checks on the given set of paths, without invoking Cargo. Paths
3238
may be either archives or object files.
3339
";
3440

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<_>>();
41+
#[derive(Debug, PartialEq)]
42+
enum Mode {
43+
BuildAndCheck,
44+
CheckOnly,
45+
}
3946

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);
47+
fn main() {
48+
let mut args_iter = env::args().skip(1);
49+
let mode = match args_iter.next() {
50+
Some(arg) if arg == "build-and-check" => Mode::BuildAndCheck,
51+
Some(arg) if arg == "check" => Mode::CheckOnly,
52+
Some(other) => invalid_usage(&format!("unrecognized mode `{other}`")),
53+
None => invalid_usage("mode must be specified"),
54+
};
55+
56+
let mut target = None;
57+
let mut verify_no_exe = true;
58+
59+
for arg in args_iter.by_ref() {
60+
match arg.as_str() {
61+
"--no-std" => verify_no_exe = false,
62+
"--" => break,
63+
f if f.starts_with("-") => invalid_usage(&format!("unrecognized flag `{f}`")),
64+
_ if mode == Mode::BuildAndCheck => target = Some(arg),
65+
_ => break,
5466
}
5567
}
56-
}
5768

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-
}
69+
let positional = args_iter.collect::<Vec<_>>();
6770

68-
let paths = exec_cargo_with_args(target, args);
69-
check_paths(&paths);
71+
match mode {
72+
Mode::BuildAndCheck => {
73+
let target = target.unwrap_or_else(|| host_target());
74+
let paths = exec_cargo_with_args(&target, positional.as_slice());
75+
check_paths(&paths, verify_no_exe);
76+
}
77+
Mode::CheckOnly => check_paths(&positional, verify_no_exe),
78+
};
79+
}
80+
81+
fn invalid_usage(s: &str) -> ! {
82+
println!("{s}\n{USAGE}");
83+
std::process::exit(1);
7084
}
7185

72-
fn check_paths<P: AsRef<Path>>(paths: &[P]) {
86+
fn check_paths<P: AsRef<Path>>(paths: &[P], verify_no_exe: bool) {
7387
for path in paths {
7488
let path = path.as_ref();
7589
println!("Checking {}", path.display());
7690
let archive = BinFile::from_path(path);
7791

78-
verify_no_duplicates(&archive);
79-
verify_core_symbols(&archive);
80-
verify_no_exec_stack(&archive);
92+
// verify_no_duplicates(&archive);
93+
// verify_core_symbols(&archive);
94+
if verify_no_exe {
95+
// We don't really have a good way of knowing whether or not an elf file is for a
96+
// no-kernel environment, in which case note.GNU-stack doesn't get emitted.
97+
verify_no_exec_stack(&archive);
98+
}
8199
}
82100
}
83101

@@ -97,15 +115,15 @@ fn host_target() -> String {
97115

98116
/// Run `cargo build` with the provided additional arguments, collecting the list of created
99117
/// libraries.
100-
fn exec_cargo_with_args(target: &str, args: &[&str]) -> Vec<PathBuf> {
118+
fn exec_cargo_with_args<S: AsRef<str>>(target: &str, args: &[S]) -> Vec<PathBuf> {
101119
let mut cmd = Command::new("cargo");
102120
cmd.args([
103121
"build",
104122
"--target",
105123
target,
106124
"--message-format=json-diagnostic-rendered-ansi",
107125
])
108-
.args(args)
126+
.args(args.iter().map(|arg| arg.as_ref()))
109127
.stdout(Stdio::piped());
110128

111129
println!("running: {cmd:?}");
@@ -300,18 +318,7 @@ fn verify_core_symbols(archive: &BinFile) {
300318
println!(" success: no undefined references to core found");
301319
}
302320

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>.
321+
/// Ensure that the object/archive will not require an executable stack.
315322
fn verify_no_exec_stack(archive: &BinFile) {
316323
let mut problem_objfiles = Vec::new();
317324

@@ -322,42 +329,91 @@ fn verify_no_exec_stack(archive: &BinFile) {
322329
});
323330

324331
if !problem_objfiles.is_empty() {
325-
panic!("the following archive members require an executable stack: {problem_objfiles:#?}");
332+
panic!("the following object files require an executable stack: {problem_objfiles:#?}");
326333
}
327334

328-
println!(" success: no writeable-executable sections found");
335+
println!(" success: no writeable+executable sections found");
329336
}
330337

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

362+
let mut return_immediate = None;
337363
let mut has_exe_sections = false;
338364
for sec in obj.sections() {
339365
let SectionFlags::Elf { sh_flags } = sec.flags() else {
340366
unreachable!("only elf files are being checked");
341367
};
342368

343-
let exe = (sh_flags & elf::SHF_EXECINSTR as u64) != 0;
369+
if sec.kind() == SectionKind::Elf(elf::SHT_ARM_ATTRIBUTES) {
370+
let data = sec.data().unwrap();
371+
parse_arm_thing(data);
372+
}
373+
374+
let is_exe = (sh_flags & elf::SHF_EXECINSTR as u64) != 0;
344375

345376
// If the magic section is present, its exe bit tells us whether or not the object
346377
// file requires an executable stack.
347378
if sec.name().unwrap_or_default() == ".note.GNU-stack" {
348-
return exe;
379+
return_immediate = Some(is_exe);
349380
}
350381

351382
// Otherwise, just keep track of whether or not we have exeuctable sections
352-
has_exe_sections |= exe;
383+
has_exe_sections |= is_exe;
384+
}
385+
386+
if let Some(imm) = return_immediate {
387+
return imm;
353388
}
354389

355390
// Ignore object files that have no executable sections, like rmeta
356391
if !has_exe_sections {
357392
return false;
358393
}
359394

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

363419
/// Thin wrapper for owning data used by `object`.
@@ -448,8 +504,12 @@ fn check_no_gnu_stack_obj() {
448504
}
449505

450506
#[test]
451-
#[cfg_attr(not(target_env = "gnu"), ignore = "requires a gnu toolchain to build")]
507+
#[cfg_attr(
508+
any(target_os = "windows", target_vendor = "apple"),
509+
ignore = "requires elf format"
510+
)]
452511
fn check_obj() {
512+
#[expect(clippy::option_env_unwrap, reason = "test is ignored")]
453513
let p = option_env!("HAS_EXE_STACK_OBJ").expect("has_exe_stack.o not present");
454514
let f = fs::read(p).unwrap();
455515
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)