Skip to content

Commit 30fd257

Browse files
committed
Use symcheck to locate writeable+executable object files
From what I have been able to find, compilers that try to emit object files compatible with a GNU linker appear to add a `.note.GNU-stack` section if the stack should not be writeable (this section is empty). We never want a writeable stack, so extend the object file check to verify that object files with any executable sections also have this `.note.GNU-stack` section. This appears to match what is done by `scanelf` to emit `!WX` [2], which is the tool used to create the output in the issue. Closes: #183 [1]: https://github.com/gentoo/pax-utils/blob/9ef54b472e42ba2c5479fbd86b8be2275724b064/scanelf.c#L428-L512
1 parent b03d96f commit 30fd257

File tree

4 files changed

+109
-9
lines changed

4 files changed

+109
-9
lines changed

crates/symbol-check/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ publish = false
88
object = "0.37.1"
99
serde_json = "1.0.140"
1010

11+
[build-dependencies]
12+
cc = "1.2.25"
13+
1114
[features]
1215
wasm = ["object/wasm"]

crates/symbol-check/build.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
fn main() {
2+
let intermediates = cc::Build::new()
3+
.file("has_wx.c")
4+
.try_compile_intermediates();
5+
if let Ok(list) = intermediates {
6+
let [obj] = list.as_slice() else {
7+
panic!(">1 output")
8+
};
9+
println!("cargo::rustc-env=HAS_WX_OBJ={}", obj.display());
10+
}
11+
}

crates/symbol-check/has_wx.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
void intermediate(void (*)(int, int), int);
2+
3+
int hack(int *array, int size) {
4+
void store (int index, int value) {
5+
array[index] = value;
6+
}
7+
8+
intermediate(store, size);
9+
}

crates/symbol-check/src/main.rs

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ use std::io::{BufRead, BufReader};
77
use std::path::{Path, PathBuf};
88
use std::process::{Command, Stdio};
99

10+
use object::elf::SHF_EXECINSTR;
1011
use object::read::archive::{ArchiveFile, ArchiveMember};
1112
use object::{
12-
File as ObjFile, Object, ObjectSection, ObjectSymbol, Symbol, SymbolKind, SymbolScope,
13+
File as ObjFile, Object, ObjectSection, ObjectSymbol, SectionFlags, Symbol, SymbolKind,
14+
SymbolScope,
1315
};
1416
use serde_json::Value;
1517

@@ -24,6 +26,10 @@ Cargo will get invoked with `CARGO_ARGS` and the specified target. All output
2426
`compiler_builtins*.rlib` files will be checked.
2527
2628
If TARGET is not specified, the host target is used.
29+
30+
check ARCHIVE_PATHS ...
31+
32+
Run the same checks on the given set of paths, without invoking Cargo.
2733
";
2834

2935
fn main() {
@@ -33,12 +39,14 @@ fn main() {
3339

3440
match &args_ref[1..] {
3541
["build-and-check", target, "--", args @ ..] if !args.is_empty() => {
36-
check_cargo_args(args);
3742
run_build_and_check(target, args);
3843
}
3944
["build-and-check", "--", args @ ..] if !args.is_empty() => {
40-
check_cargo_args(args);
41-
run_build_and_check(&host_target(), args);
45+
let target = &host_target();
46+
run_build_and_check(target, args);
47+
}
48+
["check", paths @ ..] if !paths.is_empty() => {
49+
check_paths(paths);
4250
}
4351
_ => {
4452
println!("{USAGE}");
@@ -47,25 +55,29 @@ fn main() {
4755
}
4856
}
4957

50-
/// Make sure `--target` isn't passed to avoid confusion (since it should be proivded only once,
51-
/// positionally).
52-
fn check_cargo_args(args: &[&str]) {
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).
5361
for arg in args {
5462
assert!(
5563
!arg.contains("--target"),
5664
"target must be passed positionally. {USAGE}"
5765
);
5866
}
59-
}
6067

61-
fn run_build_and_check(target: &str, args: &[&str]) {
6268
let paths = exec_cargo_with_args(target, args);
69+
check_paths(&paths);
70+
}
71+
72+
fn check_paths<P: AsRef<Path>>(paths: &[P]) {
6373
for path in paths {
74+
let path = path.as_ref();
6475
println!("Checking {}", path.display());
6576
let archive = Archive::from_path(&path);
6677

6778
verify_no_duplicates(&archive);
6879
verify_core_symbols(&archive);
80+
verify_no_exec_stack(&archive);
6981
}
7082
}
7183

@@ -288,6 +300,63 @@ fn verify_core_symbols(archive: &Archive) {
288300
println!(" success: no undefined references to core found");
289301
}
290302

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+
fn verify_no_exec_stack(archive: &Archive) {
313+
let mut problem_objfiles = Vec::new();
314+
315+
archive.for_each_object(|obj, member| {
316+
if obj_requires_exe_stack(&obj) {
317+
problem_objfiles.push(String::from_utf8_lossy(member.name()).into_owned());
318+
}
319+
});
320+
321+
if !problem_objfiles.is_empty() {
322+
panic!("the following archive members require an executable stack: {problem_objfiles:#?}");
323+
}
324+
325+
println!(" success: no writeable-executable sections found");
326+
}
327+
328+
fn obj_requires_exe_stack(obj: &ObjFile) -> bool {
329+
// Files other than elf likely do not use the same convention.
330+
if !matches!(obj, ObjFile::Elf32(_) | ObjFile::Elf64(_)) {
331+
return false;
332+
}
333+
334+
let mut has_exe_sections = false;
335+
for sec in obj.sections() {
336+
let SectionFlags::Elf { sh_flags } = sec.flags() else {
337+
unreachable!("only elf files are being checked");
338+
};
339+
340+
let exe = (sh_flags & SHF_EXECINSTR as u64) != 0;
341+
342+
// If the magic section is present, its exe bit tells us whether or not the object
343+
// file requires an executable stack.
344+
if sec.name().unwrap_or_default() == ".note.GNU-stack" {
345+
return exe;
346+
}
347+
348+
// Otherwise, just keep track of whether or not we have exeuctable sections
349+
has_exe_sections |= exe;
350+
}
351+
352+
// Ignore object files that have no executable sections, like rmeta
353+
if !has_exe_sections {
354+
return false;
355+
}
356+
357+
true
358+
}
359+
291360
/// Thin wrapper for owning data used by `object`.
292361
struct Archive {
293362
data: Vec<u8>,
@@ -325,3 +394,11 @@ impl Archive {
325394
});
326395
}
327396
}
397+
398+
#[test]
399+
fn check_obj() {
400+
let p = option_env!("HAS_WX_OBJ").expect("this platform should support the wx test build");
401+
let f = fs::read(p).unwrap();
402+
let obj = ObjFile::parse(f.as_slice()).unwrap();
403+
assert!(obj_requires_exe_stack(&obj));
404+
}

0 commit comments

Comments
 (0)