1
1
//! Tool used by CI to inspect compiler-builtins archives and help ensure we won't run into any
2
2
//! linking errors.
3
3
4
+ #![ allow( unused) ] // TODO
5
+
4
6
use std:: collections:: { BTreeMap , BTreeSet } ;
5
- use std:: fs;
6
7
use std:: io:: { BufRead , BufReader } ;
7
8
use std:: path:: { Path , PathBuf } ;
8
9
use std:: process:: { Command , Stdio } ;
10
+ use std:: { env, fs} ;
9
11
10
12
use object:: read:: archive:: ArchiveFile ;
11
13
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,
14
17
} ;
15
18
use serde_json:: Value ;
16
19
@@ -19,65 +22,80 @@ const CHECK_EXTENSIONS: &[Option<&str>] = &[Some("rlib"), Some("a"), Some("exe")
19
22
20
23
const USAGE : & str = "Usage:
21
24
22
- symbol-check build-and-check [TARGET] -- CARGO_BUILD_ARGS ...
25
+ symbol-check build-and-check [TARGET] [--no-std] -- CARGO_BUILD_ARGS ...
23
26
24
27
Cargo will get invoked with `CARGO_ARGS` and the specified target. All output
25
28
`compiler_builtins*.rlib` files will be checked.
26
29
27
30
If TARGET is not specified, the host target is used.
28
31
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 ...
30
36
31
37
Run the same checks on the given set of paths, without invoking Cargo. Paths
32
38
may be either archives or object files.
33
39
" ;
34
40
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
+ }
39
46
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 ,
54
66
}
55
67
}
56
- }
57
68
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 < _ > > ( ) ;
67
70
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 ) ;
70
84
}
71
85
72
- fn check_paths < P : AsRef < Path > > ( paths : & [ P ] ) {
86
+ fn check_paths < P : AsRef < Path > > ( paths : & [ P ] , verify_no_exe : bool ) {
73
87
for path in paths {
74
88
let path = path. as_ref ( ) ;
75
89
println ! ( "Checking {}" , path. display( ) ) ;
76
90
let archive = BinFile :: from_path ( path) ;
77
91
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
+ }
81
99
}
82
100
}
83
101
@@ -97,15 +115,15 @@ fn host_target() -> String {
97
115
98
116
/// Run `cargo build` with the provided additional arguments, collecting the list of created
99
117
/// 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 > {
101
119
let mut cmd = Command :: new ( "cargo" ) ;
102
120
cmd. args ( [
103
121
"build" ,
104
122
"--target" ,
105
123
target,
106
124
"--message-format=json-diagnostic-rendered-ansi" ,
107
125
] )
108
- . args ( args)
126
+ . args ( args. iter ( ) . map ( |arg| arg . as_ref ( ) ) )
109
127
. stdout ( Stdio :: piped ( ) ) ;
110
128
111
129
println ! ( "running: {cmd:?}" ) ;
@@ -300,18 +318,7 @@ fn verify_core_symbols(archive: &BinFile) {
300
318
println ! ( " success: no undefined references to core found" ) ;
301
319
}
302
320
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.
315
322
fn verify_no_exec_stack ( archive : & BinFile ) {
316
323
let mut problem_objfiles = Vec :: new ( ) ;
317
324
@@ -322,42 +329,91 @@ fn verify_no_exec_stack(archive: &BinFile) {
322
329
} ) ;
323
330
324
331
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:#?}" ) ;
326
333
}
327
334
328
- println ! ( " success: no writeable- executable sections found" ) ;
335
+ println ! ( " success: no writeable+ executable sections found" ) ;
329
336
}
330
337
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>
331
356
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.
333
358
if obj. format ( ) != BinaryFormat :: Elf {
334
359
return false ;
335
360
}
336
361
362
+ let mut return_immediate = None ;
337
363
let mut has_exe_sections = false ;
338
364
for sec in obj. sections ( ) {
339
365
let SectionFlags :: Elf { sh_flags } = sec. flags ( ) else {
340
366
unreachable ! ( "only elf files are being checked" ) ;
341
367
} ;
342
368
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 ;
344
375
345
376
// If the magic section is present, its exe bit tells us whether or not the object
346
377
// file requires an executable stack.
347
378
if sec. name ( ) . unwrap_or_default ( ) == ".note.GNU-stack" {
348
- return exe ;
379
+ return_immediate = Some ( is_exe ) ;
349
380
}
350
381
351
382
// 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;
353
388
}
354
389
355
390
// Ignore object files that have no executable sections, like rmeta
356
391
if !has_exe_sections {
357
392
return false ;
358
393
}
359
394
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() {}
361
417
}
362
418
363
419
/// Thin wrapper for owning data used by `object`.
@@ -448,8 +504,12 @@ fn check_no_gnu_stack_obj() {
448
504
}
449
505
450
506
#[ 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
+ ) ]
452
511
fn check_obj ( ) {
512
+ #[ expect( clippy:: option_env_unwrap, reason = "test is ignored" ) ]
453
513
let p = option_env ! ( "HAS_EXE_STACK_OBJ" ) . expect ( "has_exe_stack.o not present" ) ;
454
514
let f = fs:: read ( p) . unwrap ( ) ;
455
515
let obj = ObjFile :: parse ( f. as_slice ( ) ) . unwrap ( ) ;
0 commit comments