diff --git a/crates/symbol-check/Cargo.toml b/crates/symbol-check/Cargo.toml index 30969ee40..991908ac9 100644 --- a/crates/symbol-check/Cargo.toml +++ b/crates/symbol-check/Cargo.toml @@ -11,3 +11,6 @@ serde_json = "1.0.140" [features] wasm = ["object/wasm"] + +[build-dependencies] +cc = "1.2.25" diff --git a/crates/symbol-check/build.rs b/crates/symbol-check/build.rs new file mode 100644 index 000000000..d7eb4afba --- /dev/null +++ b/crates/symbol-check/build.rs @@ -0,0 +1,9 @@ +fn main() { + let intermediates = cc::Build::new() + .file("has_wx.c") + .try_compile_intermediates(); + if let Ok(list) = intermediates { + let [obj] = list.as_slice() else { panic!() }; + println!("cargo::rustc-env=HAS_WX_OBJ={}", obj.display()); + } +} diff --git a/crates/symbol-check/has_wx.c b/crates/symbol-check/has_wx.c new file mode 100644 index 000000000..112afb1ba --- /dev/null +++ b/crates/symbol-check/has_wx.c @@ -0,0 +1,9 @@ +void intermediate(void (*)(int, int), int); + +int hack(int *array, int size) { + void store (int index, int value) { + array[index] = value; + } + + intermediate(store, size); +} diff --git a/crates/symbol-check/src/main.rs b/crates/symbol-check/src/main.rs index d83cd318d..3c3b254ef 100644 --- a/crates/symbol-check/src/main.rs +++ b/crates/symbol-check/src/main.rs @@ -7,8 +7,12 @@ use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use object::elf::SHF_EXECINSTR; use object::read::archive::{ArchiveFile, ArchiveMember}; -use object::{Object, ObjectSymbol, Symbol, SymbolKind, SymbolScope, SymbolSection}; +use object::{ + File as ObjFile, Object, ObjectSection, ObjectSymbol, SectionFlags, Symbol, SymbolKind, + SymbolScope, SymbolSection, +}; use serde_json::Value; const CHECK_LIBRARIES: &[&str] = &["compiler_builtins", "builtins_test_intrinsics"]; @@ -28,13 +32,11 @@ fn main() { let args_ref = args.iter().map(String::as_str).collect::>(); match &args_ref[1..] { - ["build-and-check", rest @ ..] if !rest.is_empty() => { - let paths = exec_cargo_with_args(rest); - for path in paths { - println!("Checking {}", path.display()); - verify_no_duplicates(&path); - verify_core_symbols(&path); - } + ["build-and-check", "--target", target, args @ ..] if !args.is_empty() => { + run_build_and_check(Some(target), args); + } + ["build-and-check", args @ ..] if !args.is_empty() => { + run_build_and_check(None, args); } _ => { println!("{USAGE}"); @@ -43,12 +45,43 @@ fn main() { } } +fn run_build_and_check(target: Option<&str>, args: &[&str]) { + let paths = exec_cargo_with_args(target, args); + for path in paths { + println!("Checking {}", path.display()); + let archive = Archive::from_path(&path); + + verify_no_duplicates(&archive); + verify_core_symbols(&archive); + verify_no_exec_stack(&archive); + } +} + +fn host_target() -> String { + let out = Command::new("rustc") + .arg("--version") + .arg("--verbose") + .output() + .unwrap(); + assert!(out.status.success()); + let out = String::from_utf8(out.stdout).unwrap(); + out.lines() + .find_map(|s| s.strip_prefix("host: ")) + .unwrap() + .to_owned() +} + /// Run `cargo build` with the provided additional arguments, collecting the list of created /// libraries. -fn exec_cargo_with_args(args: &[&str]) -> Vec { +fn exec_cargo_with_args(target: Option<&str>, args: &[&str]) -> Vec { + let mut host = String::new(); + let target = target.unwrap_or_else(|| { + host = host_target(); + host.as_str() + }); + let mut cmd = Command::new("cargo"); - cmd.arg("build") - .arg("--message-format=json") + cmd.args(["build", "--target", target, "--message-format=json"]) .args(args) .stdout(Stdio::piped()); @@ -133,12 +166,12 @@ impl SymInfo { /// Note that this will also locate cases where a symbol is weakly defined in more than one place. /// Technically there are no linker errors that will come from this, but it keeps our binary more /// straightforward and saves some distribution size. -fn verify_no_duplicates(path: &Path) { +fn verify_no_duplicates(archive: &Archive) { let mut syms = BTreeMap::::new(); let mut dups = Vec::new(); let mut found_any = false; - for_each_symbol(path, |symbol, member| { + archive.for_each_symbol(|symbol, member| { // Only check defined globals if !symbol.is_global() || symbol.is_undefined() { return; @@ -185,12 +218,12 @@ fn verify_no_duplicates(path: &Path) { } /// Ensure that there are no references to symbols from `core` that aren't also (somehow) defined. -fn verify_core_symbols(path: &Path) { +fn verify_core_symbols(archive: &Archive) { let mut defined = BTreeSet::new(); let mut undefined = Vec::new(); let mut has_symbols = false; - for_each_symbol(path, |symbol, member| { + archive.for_each_symbol(|symbol, member| { has_symbols = true; // Find only symbols from `core` @@ -219,14 +252,101 @@ fn verify_core_symbols(path: &Path) { println!(" success: no undefined references to core found"); } -/// For a given archive path, do something with each symbol. -fn for_each_symbol(path: &Path, mut f: impl FnMut(Symbol, &ArchiveMember)) { - let data = fs::read(path).expect("reading file failed"); - let archive = ArchiveFile::parse(data.as_slice()).expect("archive parse failed"); - for member in archive.members() { - let member = member.expect("failed to access member"); - let obj_data = member.data(&*data).expect("failed to access object"); - let obj = object::File::parse(obj_data).expect("failed to parse object"); - obj.symbols().for_each(|sym| f(sym, &member)); +/// Check that all object files contain a section named `.note.GNU-stack`, indicating a +/// nonexecutable stack. +fn verify_no_exec_stack(archive: &Archive) { + let mut problem_objfiles = Vec::new(); + + archive.for_each_object(|obj, member| { + if obj_has_exe_stack(&obj) { + problem_objfiles.push(String::from_utf8_lossy(member.name()).into_owned()); + } + }); + + if !problem_objfiles.is_empty() { + panic!( + "the following archive members have executable sections but no \ + `.note.GNU-stack` section: {problem_objfiles:#?}" + ); + } + + println!(" success: no writeable-executable sections found"); +} + +fn obj_has_exe_stack(obj: &ObjFile) -> bool { + // Files other than elf likely do not use the same convention. + if !matches!(obj, ObjFile::Elf32(_) | ObjFile::Elf64(_)) { + return false; } + + let mut has_exe_sections = false; + for sec in obj.sections() { + let SectionFlags::Elf { sh_flags } = sec.flags() else { + unreachable!("only elf files are being checked"); + }; + + let exe = (sh_flags & SHF_EXECINSTR as u64) != 0; + has_exe_sections |= exe; + + // Located a GNU-stack section, nothing else to do + if sec.name().unwrap_or_default() == ".note.GNU-stack" { + return false; + } + } + + // Ignore object files that have no executable sections, like rmeta + if !has_exe_sections { + return false; + } + + true +} + +/// Thin wrapper for owning data used by `object`. +struct Archive { + data: Vec, +} + +impl Archive { + fn from_path(path: &Path) -> Self { + Self { + data: fs::read(path).expect("reading file failed"), + } + } + + fn file(&self) -> ArchiveFile<'_> { + ArchiveFile::parse(self.data.as_slice()).expect("archive parse failed") + } + + /// For a given archive, do something with each object file. + fn for_each_object(&self, mut f: impl FnMut(ObjFile, &ArchiveMember)) { + let archive = self.file(); + + for member in archive.members() { + let member = member.expect("failed to access member"); + let obj_data = member + .data(self.data.as_slice()) + .expect("failed to access object"); + let obj = ObjFile::parse(obj_data).expect("failed to parse object"); + f(obj, &member); + } + } + + /// For a given archive, do something with each symbol. + fn for_each_symbol(&self, mut f: impl FnMut(Symbol, &ArchiveMember)) { + self.for_each_object(|obj, member| { + obj.symbols().for_each(|sym| f(sym, member)); + }); + } +} + +#[test] +fn check_obj() { + let Some(p) = option_env!("HAS_WX_OBJ") else { + return; + }; + + let f = fs::read(p).unwrap(); + let obj = ObjFile::parse(f.as_slice()).unwrap(); + assert!(obj_has_exe_stack(&obj)); }