Skip to content

Commit 250b8bc

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 update update debug update fix asm file
1 parent a614590 commit 250b8bc

File tree

5 files changed

+154
-2
lines changed

5 files changed

+154
-2
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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//! Compile test sources to object files.
2+
3+
use std::env;
4+
5+
fn main() {
6+
let compiler = cc::Build::new().get_compiler();
7+
8+
println!(
9+
"cargo::rustc-env=OBJ_TARGET={}",
10+
env::var("TARGET").unwrap()
11+
);
12+
13+
let objs = cc::Build::new()
14+
.file("tests/no_gnu_stack.S")
15+
.compile_intermediates();
16+
let [obj] = objs.as_slice() else {
17+
panic!(">1 output")
18+
};
19+
println!("cargo::rustc-env=NO_GNU_STACK_OBJ={}", obj.display());
20+
21+
if !compiler.is_like_gnu() {
22+
println!("cargo::warning=Can't run execstack test; non-GNU compiler");
23+
return;
24+
}
25+
26+
let objs = cc::Build::new()
27+
.file("tests/has_exe_stack.c")
28+
.compile_intermediates();
29+
let [obj] = objs.as_slice() else {
30+
panic!(">1 output")
31+
};
32+
println!("cargo::rustc-env=HAS_EXE_STACK_OBJ={}", obj.display());
33+
}

crates/symbol-check/src/main.rs

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use std::process::{Command, Stdio};
99

1010
use object::read::archive::ArchiveFile;
1111
use object::{
12-
File as ObjFile, Object, ObjectSection, ObjectSymbol, Result as ObjResult, Symbol, SymbolKind,
13-
SymbolScope,
12+
BinaryFormat, File as ObjFile, Object, ObjectSection, ObjectSymbol, Result as ObjResult,
13+
SectionFlags, Symbol, SymbolKind, SymbolScope, elf,
1414
};
1515
use serde_json::Value;
1616

@@ -77,6 +77,7 @@ fn check_paths<P: AsRef<Path>>(paths: &[P]) {
7777

7878
verify_no_duplicates(&archive);
7979
verify_core_symbols(&archive);
80+
verify_no_exec_stack(&archive);
8081
}
8182
}
8283

@@ -299,6 +300,66 @@ fn verify_core_symbols(archive: &BinFile) {
299300
println!(" success: no undefined references to core found");
300301
}
301302

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>.
315+
fn verify_no_exec_stack(archive: &BinFile) {
316+
let mut problem_objfiles = Vec::new();
317+
318+
archive.for_each_object(|obj, obj_path| {
319+
if obj_requires_exe_stack(&obj) {
320+
problem_objfiles.push(obj_path.to_owned());
321+
}
322+
});
323+
324+
if !problem_objfiles.is_empty() {
325+
panic!("the following archive members require an executable stack: {problem_objfiles:#?}");
326+
}
327+
328+
println!(" success: no writeable-executable sections found");
329+
}
330+
331+
fn obj_requires_exe_stack(obj: &ObjFile) -> bool {
332+
// Files other than elf likely do not use the same convention.
333+
if obj.format() != BinaryFormat::Elf {
334+
return false;
335+
}
336+
337+
let mut has_exe_sections = false;
338+
for sec in obj.sections() {
339+
let SectionFlags::Elf { sh_flags } = sec.flags() else {
340+
unreachable!("only elf files are being checked");
341+
};
342+
343+
let exe = (sh_flags & elf::SHF_EXECINSTR as u64) != 0;
344+
345+
// If the magic section is present, its exe bit tells us whether or not the object
346+
// file requires an executable stack.
347+
if sec.name().unwrap_or_default() == ".note.GNU-stack" {
348+
return exe;
349+
}
350+
351+
// Otherwise, just keep track of whether or not we have exeuctable sections
352+
has_exe_sections |= exe;
353+
}
354+
355+
// Ignore object files that have no executable sections, like rmeta
356+
if !has_exe_sections {
357+
return false;
358+
}
359+
360+
true
361+
}
362+
302363
/// Thin wrapper for owning data used by `object`.
303364
struct BinFile {
304365
path: PathBuf,
@@ -360,3 +421,43 @@ impl BinFile {
360421
});
361422
}
362423
}
424+
425+
/// Check with a binary that has no `.note.GNU-stack` section, indicating platform-default stack
426+
/// writeability.
427+
#[test]
428+
fn check_no_gnu_stack_obj() {
429+
// Should be supported on all Unix platforms
430+
let p = env!("NO_GNU_STACK_OBJ");
431+
let f = fs::read(p).unwrap();
432+
let obj = ObjFile::parse(f.as_slice()).unwrap();
433+
dbg!(
434+
obj.format(),
435+
obj.architecture(),
436+
obj.sub_architecture(),
437+
obj.is_64()
438+
);
439+
let has_exe_stack = obj_requires_exe_stack(&obj);
440+
441+
let obj_target = env!("OBJ_TARGET");
442+
if obj_target.contains("-windows-") || obj_target.contains("-apple-") {
443+
// Non-ELF targets don't have executable stacks marked in the same way
444+
assert!(!has_exe_stack);
445+
} else {
446+
assert!(has_exe_stack);
447+
}
448+
}
449+
450+
#[test]
451+
#[cfg_attr(not(target_env = "gnu"), ignore = "requires a gnu toolchain to build")]
452+
fn check_obj() {
453+
let p = option_env!("HAS_EXE_STACK_OBJ").expect("has_exe_stack.o not present");
454+
let f = fs::read(p).unwrap();
455+
let obj = ObjFile::parse(f.as_slice()).unwrap();
456+
dbg!(
457+
obj.format(),
458+
obj.architecture(),
459+
obj.sub_architecture(),
460+
obj.is_64()
461+
);
462+
assert!(obj_requires_exe_stack(&obj));
463+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* GNU nested functions are the only way I could fine to force an executable
2+
* stack. Supported by GCC only, not Clang. */
3+
4+
void intermediate(void (*)(int, int), int);
5+
6+
int hack(int *array, int size) {
7+
void store (int index, int value) {
8+
array[index] = value;
9+
}
10+
11+
intermediate(store, size);
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/* Assembly files do not get a `.note.GNU-stack` section, meaning platform-specific
2+
* stack executability (usually yes). */
3+
nop

0 commit comments

Comments
 (0)