55//! actual target is cross compiled.
66
77use std:: collections:: { BTreeMap , BTreeSet , HashSet } ;
8- use std:: fs;
98use std:: io:: { BufRead , BufReader } ;
109use std:: path:: { Path , PathBuf } ;
1110use std:: process:: { Command , Stdio , exit} ;
1211use std:: sync:: LazyLock ;
12+ use std:: { env, fs} ;
1313
1414use object:: read:: archive:: ArchiveFile ;
1515use object:: {
16- BinaryFormat , File as ObjFile , Object , ObjectSection , ObjectSymbol , Result as ObjResult ,
17- Symbol , SymbolKind , SymbolScope ,
16+ Architecture , BinaryFormat , Endianness , File as ObjFile , Object , ObjectSection , ObjectSymbol ,
17+ Result as ObjResult , SectionFlags , Symbol , SymbolKind , SymbolScope , U32 , elf ,
1818} ;
1919use regex:: Regex ;
2020use serde_json:: Value ;
2121
2222const CHECK_LIBRARIES : & [ & str ] = & [ "compiler_builtins" , "builtins_test_intrinsics" ] ;
2323const CHECK_EXTENSIONS : & [ Option < & str > ] = & [ Some ( "rlib" ) , Some ( "a" ) , Some ( "exe" ) , None ] ;
24+ const GNU_STACK : & str = ".note.GNU-stack" ;
2425
2526const USAGE : & str = "Usage:
2627
27- symbol-check --build-and-check [--target TARGET] -- CARGO_BUILD_ARGS ...
28+ symbol-check --build-and-check [--target TARGET] [--no-os] -- CARGO_BUILD_ARGS ...
2829 symbol-check --check PATHS ...\
2930 ";
3031
@@ -51,6 +52,12 @@ fn main() {
5152 "Run checks on the given set of paths, without invoking Cargo. Paths \
5253 may be either archives or object files.",
5354 ) ;
55+ opts. optflag (
56+ "" ,
57+ "no-os" ,
58+ "The binaries will not be checked for executable stacks. Used for embedded targets which \
59+ don't set `.note.GNU-stack` since there is no protection.",
60+ ) ;
5461
5562 let print_usage_and_exit = |code : i32 | -> ! {
5663 eprintln ! ( "{}" , opts. usage( USAGE ) ) ;
@@ -66,6 +73,7 @@ fn main() {
6673 print_usage_and_exit ( 0 ) ;
6774 }
6875
76+ let no_os_target = m. opt_present ( "no-os" ) ;
6977 let free_args = m. free . iter ( ) . map ( String :: as_str) . collect :: < Vec < _ > > ( ) ;
7078 for arg in & free_args {
7179 assert ! (
@@ -77,25 +85,26 @@ fn main() {
7785 if m. opt_present ( "build-and-check" ) {
7886 let target = m. opt_str ( "target" ) . unwrap_or ( env ! ( "HOST" ) . to_string ( ) ) ;
7987 let paths = exec_cargo_with_args ( & target, & free_args) ;
80- check_paths ( & paths) ;
88+ check_paths ( & paths, no_os_target ) ;
8189 } else if m. opt_present ( "check" ) {
8290 if free_args. is_empty ( ) {
8391 print_usage_and_exit ( 1 ) ;
8492 }
85- check_paths ( & free_args) ;
93+ check_paths ( & free_args, no_os_target ) ;
8694 } else {
8795 print_usage_and_exit ( 1 ) ;
8896 }
8997}
9098
91- fn check_paths < P : AsRef < Path > > ( paths : & [ P ] ) {
99+ fn check_paths < P : AsRef < Path > > ( paths : & [ P ] , no_os_target : bool ) {
92100 for path in paths {
93101 let path = path. as_ref ( ) ;
94102 println ! ( "Checking {}" , path. display( ) ) ;
95103 let archive = BinFile :: from_path ( path) ;
96104
97105 verify_no_duplicates ( & archive) ;
98106 verify_core_symbols ( & archive) ;
107+ verify_no_exec_stack ( & archive, no_os_target) ;
99108 }
100109}
101110
@@ -320,6 +329,177 @@ fn verify_core_symbols(archive: &BinFile) {
320329 println ! ( " success: no undefined references to core found" ) ;
321330}
322331
332+ /// Reasons a binary is considered to have an executable stack.
333+ enum ExeStack {
334+ MissingGnuStackSec ,
335+ ExeGnuStackSec ,
336+ ExePtGnuStack ,
337+ }
338+
339+ /// Ensure that the object/archive will not require an executable stack.
340+ fn verify_no_exec_stack ( archive : & BinFile , no_os_target : bool ) {
341+ if no_os_target {
342+ // We don't really have a good way of knowing whether or not an elf file is for a
343+ // no-os environment so we rely on a CLI arg (note.GNU-stack doesn't get emitted if
344+ // there is no OS to protect the stack).
345+ println ! ( " skipping check for writeable+executable stack on no-os target" ) ;
346+ return ;
347+ }
348+
349+ let mut problem_objfiles = Vec :: new ( ) ;
350+
351+ archive. for_each_object ( |obj, obj_path| match check_obj_exe_stack ( & obj) {
352+ Ok ( ( ) ) => ( ) ,
353+ Err ( exe) => problem_objfiles. push ( ( obj_path. to_owned ( ) , exe) ) ,
354+ } ) ;
355+
356+ if problem_objfiles. is_empty ( ) {
357+ println ! ( " success: no writeable+executable stack indicators found" ) ;
358+ return ;
359+ }
360+
361+ eprintln ! ( "the following object files require an executable stack:" ) ;
362+
363+ for ( obj, exe) in problem_objfiles {
364+ let reason = match exe {
365+ ExeStack :: MissingGnuStackSec => "no .note.GNU-stack section" ,
366+ ExeStack :: ExeGnuStackSec => ".note.GNU-stack section marked SHF_EXECINSTR" ,
367+ ExeStack :: ExePtGnuStack => "PT_GNU_STACK program header marked PF_X" ,
368+ } ;
369+ eprintln ! ( " {obj} ({reason})" ) ;
370+ }
371+
372+ exit ( 1 ) ;
373+ }
374+
375+ /// `Err` if the section/flag combination indicates that the object file should be linked with an
376+ /// executable stack.
377+ fn check_obj_exe_stack ( obj : & ObjFile ) -> Result < ( ) , ExeStack > {
378+ match obj. format ( ) {
379+ BinaryFormat :: Elf => check_elf_exe_stack ( obj) ,
380+ // Technically has the `MH_ALLOW_STACK_EXECUTION` flag but I can't get the compiler to
381+ // emit it (`-allow_stack_execute` doesn't seem to work in recent versions).
382+ BinaryFormat :: MachO => Ok ( ( ) ) ,
383+ // Can't find much information about Windows stack executability.
384+ BinaryFormat :: Coff | BinaryFormat :: Pe => Ok ( ( ) ) ,
385+ // Also not sure about wasm.
386+ BinaryFormat :: Wasm => Ok ( ( ) ) ,
387+ BinaryFormat :: Xcoff | _ => {
388+ unimplemented ! ( "binary format {:?} is not supported" , obj. format( ) )
389+ }
390+ }
391+ }
392+
393+ /// Check for an executable stack in elf binaries.
394+ ///
395+ /// If the `PT_GNU_STACK` header on a binary is present and marked executable, the binary will
396+ /// have an executable stack (RWE rather than the desired RW). If any object file has the right
397+ /// `.note.GNU-stack` logic, the final binary will get `PT_GNU_STACK`.
398+ ///
399+ /// Individual object file logic is as follows, paraphrased from [1]:
400+ ///
401+ /// - A `.note.GNU-stack` section with the exe flag means this needs an executable stack
402+ /// - A `.note.GNU-stack` section without the exe flag means there is no executable stack needed
403+ /// - Without the section, behavior is target-specific. Historically it usually means an executable
404+ /// stack is required.
405+ ///
406+ /// Per [2], it is now deprecated behavior for a missing `.note.GNU-stack` section to imply an
407+ /// executable stack. However, we shouldn't assume that tooling has caught up to this.
408+ ///
409+ /// [1]: https://www.man7.org/linux/man-pages/man1/ld.1.html
410+ /// [2]: https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=0d38576a34ec64a1b4500c9277a8e9d0f07e6774>
411+ fn check_elf_exe_stack ( obj : & ObjFile ) -> Result < ( ) , ExeStack > {
412+ let end = obj. endianness ( ) ;
413+
414+ // Check for PT_GNU_STACK marked executable
415+ let mut is_obj_exe = false ;
416+ let mut found_gnu_stack = false ;
417+ let mut check_ph = |p_type : U32 < Endianness > , p_flags : U32 < Endianness > | {
418+ let ty = p_type. get ( end) ;
419+ let flags = p_flags. get ( end) ;
420+
421+ // Presence of PT_INTERP indicates that this is an executable rather than a standalone
422+ // object file.
423+ if ty == elf:: PT_INTERP {
424+ is_obj_exe = true ;
425+ }
426+
427+ if ty == elf:: PT_GNU_STACK {
428+ assert ! ( !found_gnu_stack, "multiple PT_GNU_STACK sections" ) ;
429+ found_gnu_stack = true ;
430+ if flags & elf:: PF_X != 0 {
431+ return Err ( ExeStack :: ExePtGnuStack ) ;
432+ }
433+ }
434+
435+ Ok ( ( ) )
436+ } ;
437+
438+ match obj {
439+ ObjFile :: Elf32 ( f) => {
440+ for ph in f. elf_program_headers ( ) {
441+ check_ph ( ph. p_type , ph. p_flags ) ?;
442+ }
443+ }
444+ ObjFile :: Elf64 ( f) => {
445+ for ph in f. elf_program_headers ( ) {
446+ check_ph ( ph. p_type , ph. p_flags ) ?;
447+ }
448+ }
449+ _ => panic ! ( "should only be called with elf objects" ) ,
450+ }
451+
452+ if is_obj_exe {
453+ return Ok ( ( ) ) ;
454+ }
455+
456+ // The remaining are checks for individual object files, which wind up controlling PT_GNU_STACK
457+ // in the final binary.
458+ let mut gnu_stack_exe = None ;
459+ let mut has_exe_sections = false ;
460+ for sec in obj. sections ( ) {
461+ let SectionFlags :: Elf { sh_flags } = sec. flags ( ) else {
462+ unreachable ! ( "only elf files are being checked" ) ;
463+ } ;
464+
465+ let is_sec_exe = sh_flags & u64:: from ( elf:: SHF_EXECINSTR ) != 0 ;
466+
467+ // If the magic section is present, its exe bit tells us whether or not the object
468+ // file requires an executable stack.
469+ if sec. name ( ) . unwrap_or_default ( ) == GNU_STACK {
470+ assert ! ( gnu_stack_exe. is_none( ) , "multiple {GNU_STACK} sections" ) ;
471+ if is_sec_exe {
472+ gnu_stack_exe = Some ( Err ( ExeStack :: ExeGnuStackSec ) ) ;
473+ } else {
474+ gnu_stack_exe = Some ( Ok ( ( ) ) ) ;
475+ }
476+ }
477+
478+ // Otherwise, just keep track of whether or not we have exeuctable sections
479+ has_exe_sections |= is_sec_exe;
480+ }
481+
482+ // GNU_STACK sets the executability if specified.
483+ if let Some ( exe) = gnu_stack_exe {
484+ return exe;
485+ }
486+
487+ // Ignore object files that have no executable sections, like rmeta.
488+ if !has_exe_sections {
489+ return Ok ( ( ) ) ;
490+ }
491+
492+ // If there is no `.note.GNU-stack` and no executable sections, behavior differs by platform.
493+ match obj. architecture ( ) {
494+ // PPC64 doesn't set `.note.GNU-stack` since GNU nested functions don't need a trampoline,
495+ // <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=21098>. From experimentation, it seems
496+ // like this only applies to big endian.
497+ Architecture :: PowerPc64 if obj. endianness ( ) == Endianness :: Big => Ok ( ( ) ) ,
498+
499+ _ => Err ( ExeStack :: MissingGnuStackSec ) ,
500+ }
501+ }
502+
323503/// Thin wrapper for owning data used by `object`.
324504struct BinFile {
325505 path : PathBuf ,
0 commit comments