1- use anyhow:: bail;
2- use wasm_metadata:: Producers ;
3- use wasmparser:: { Encoding , ExternalKind , Parser , Payload } ;
1+ use crate :: module_info:: ModuleInfo ;
42
5- /// Represents the detected likelihood of the allocation bug fixed in
6- /// https://github.com/WebAssembly/wasi-libc/pull/377 being present in a Wasm
7- /// module.
3+ pub const EARLIEST_PROBABLY_SAFE_CLANG_VERSION : & str = "15.0.7" ;
4+
5+ /// This error represents the likely presence of the allocation bug fixed in
6+ /// https://github.com/WebAssembly/wasi-libc/pull/377 in a Wasm module.
87#[ derive( Debug , PartialEq ) ]
9- pub enum WasiLibc377Bug {
10- ProbablySafe ,
11- ProbablyUnsafe ,
12- Unknown ,
8+ pub struct WasiLibc377Bug {
9+ clang_version : Option < String > ,
1310}
1411
1512impl WasiLibc377Bug {
16- pub fn detect ( module : & [ u8 ] ) -> anyhow:: Result < Self > {
17- for payload in Parser :: new ( 0 ) . parse_all ( module) {
18- match payload? {
19- Payload :: Version { encoding, .. } if encoding != Encoding :: Module => {
20- bail ! ( "detection only applicable to modules" ) ;
21- }
22- Payload :: ExportSection ( reader) => {
23- for export in reader {
24- let export = export?;
25- if export. kind == ExternalKind :: Func && export. name == "cabi_realloc" {
26- // `cabi_realloc` is a good signal that this module
27- // uses wit-bindgen, making it probably-safe.
28- tracing:: debug!( "Found cabi_realloc export" ) ;
29- return Ok ( Self :: ProbablySafe ) ;
30- }
31- }
32- }
33- Payload :: CustomSection ( c) if c. name ( ) == "producers" => {
34- let producers = Producers :: from_bytes ( c. data ( ) , c. data_offset ( ) ) ?;
35- if let Some ( clang_version) =
36- producers. get ( "processed-by" ) . and_then ( |f| f. get ( "clang" ) )
37- {
38- tracing:: debug!( clang_version, "Parsed producers.processed-by.clang" ) ;
39-
40- // Clang/LLVM version is a good proxy for wasi-sdk
41- // version; the allocation bug was fixed in wasi-sdk-18
42- // and LLVM was updated to 15.0.7 in wasi-sdk-19.
43- if let Some ( ( major, minor, patch) ) = parse_clang_version ( clang_version) {
44- return if ( major, minor, patch) >= ( 15 , 0 , 7 ) {
45- Ok ( Self :: ProbablySafe )
46- } else {
47- Ok ( Self :: ProbablyUnsafe )
48- } ;
49- } else {
50- tracing:: warn!(
51- clang_version,
52- "Unexpected producers.processed-by.clang version"
53- ) ;
54- }
55- }
56- }
57- _ => ( ) ,
13+ /// Detects the likely presence of this bug.
14+ pub fn check ( module_info : & ModuleInfo ) -> Result < ( ) , Self > {
15+ if module_info. probably_uses_wit_bindgen ( ) {
16+ // Modules built with wit-bindgen are probably safe.
17+ return Ok ( ( ) ) ;
18+ }
19+ if let Some ( clang_version) = & module_info. clang_version {
20+ // Clang/LLVM version is a good proxy for wasi-sdk
21+ // version; the allocation bug was fixed in wasi-sdk-18
22+ // and LLVM was updated to 15.0.7 in wasi-sdk-19.
23+ if let Some ( ( major, minor, patch) ) = parse_clang_version ( clang_version) {
24+ let earliest_safe =
25+ parse_clang_version ( EARLIEST_PROBABLY_SAFE_CLANG_VERSION ) . unwrap ( ) ;
26+ if ( major, minor, patch) >= earliest_safe {
27+ return Ok ( ( ) ) ;
28+ } else {
29+ return Err ( Self {
30+ clang_version : Some ( clang_version. clone ( ) ) ,
31+ } ) ;
32+ } ;
33+ } else {
34+ tracing:: warn!(
35+ clang_version,
36+ "Unexpected producers.processed-by.clang version"
37+ ) ;
5838 }
5939 }
60- Ok ( Self :: Unknown )
40+ // If we can't assert that the module uses wit-bindgen OR was compiled
41+ // with a new-enough wasi-sdk, conservatively assume it may be buggy.
42+ Err ( Self {
43+ clang_version : None ,
44+ } )
6145 }
6246}
6347
48+ impl std:: fmt:: Display for WasiLibc377Bug {
49+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
50+ write ! (
51+ f,
52+ "This Wasm module may have been compiled with wasi-sdk version <19 which \
53+ contains a critical memory safety bug. For more information, see: \
54+ https://github.com/fermyon/spin/issues/2552"
55+ )
56+ }
57+ }
58+
59+ impl std:: error:: Error for WasiLibc377Bug { }
60+
6461fn parse_clang_version ( ver : & str ) -> Option < ( u16 , u16 , u16 ) > {
6562 // Strip optional trailing detail after space
6663 let ver = ver. split ( ' ' ) . next ( ) . unwrap ( ) ;
@@ -77,42 +74,42 @@ mod tests {
7774
7875 #[ test]
7976 fn wasi_libc_377_detect ( ) {
80- use WasiLibc377Bug :: * ;
81- for ( wasm, expected) in [
82- ( r#"(module)"# , Unknown ) ,
77+ for ( wasm, safe) in [
78+ ( r#"(module)"# , false ) ,
8379 (
8480 r#"(module (func (export "cabi_realloc") (unreachable)))"# ,
85- ProbablySafe ,
81+ true ,
8682 ) ,
8783 (
8884 r#"(module (func (export "some_other_function") (unreachable)))"# ,
89- Unknown ,
85+ false ,
9086 ) ,
9187 (
9288 r#"(module (@producers (processed-by "clang" "16.0.0 extra-stuff")))"# ,
93- ProbablySafe ,
89+ true ,
9490 ) ,
9591 (
9692 r#"(module (@producers (processed-by "clang" "15.0.7")))"# ,
97- ProbablySafe ,
93+ true ,
9894 ) ,
9995 (
10096 r#"(module (@producers (processed-by "clang" "15.0.6")))"# ,
101- ProbablyUnsafe ,
97+ false ,
10298 ) ,
10399 (
104- r#"(module (@producers (processed-by "clang" "14.0.0")))"# ,
105- ProbablyUnsafe ,
100+ r#"(module (@producers (processed-by "clang" "14.0.0 extra-stuff ")))"# ,
101+ false ,
106102 ) ,
107103 (
108104 r#"(module (@producers (processed-by "clang" "a.b.c")))"# ,
109- Unknown ,
105+ false ,
110106 ) ,
111107 ] {
112108 eprintln ! ( "WAT: {wasm}" ) ;
113109 let module = wat:: parse_str ( wasm) . unwrap ( ) ;
114- let detected = WasiLibc377Bug :: detect ( & module) . unwrap ( ) ;
115- assert_eq ! ( detected, expected) ;
110+ let module_info = ModuleInfo :: from_module ( & module) . unwrap ( ) ;
111+ let detected = WasiLibc377Bug :: check ( & module_info) ;
112+ assert ! ( detected. is_ok( ) == safe, "{wasm} -> {detected:?}" ) ;
116113 }
117114 }
118115}
0 commit comments