From 5c9f8981ae1286d7e7379a60cb38a6464f3ada52 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 30 Nov 2023 17:42:22 +0100 Subject: [PATCH] Check runtime and compiled Godot versions for compatibility Related changes: - Remove legacy 4.0.x special case with patch == 999. - Fix UB with reading too far beyond function pointer. - Fix UB with reading entire struct at once. --- godot-ffi/src/compat/compat_4_1.rs | 116 +++++++++++++++-------------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/godot-ffi/src/compat/compat_4_1.rs b/godot-ffi/src/compat/compat_4_1.rs index e0eb7aad2..1abf7eec2 100644 --- a/godot-ffi/src/compat/compat_4_1.rs +++ b/godot-ffi/src/compat/compat_4_1.rs @@ -16,19 +16,9 @@ use crate::compat::BindingCompat; pub type InitCompat = sys::GDExtensionInterfaceGetProcAddress; -#[cfg(not(target_family = "wasm"))] -#[repr(C)] -struct LegacyLayout { - version_major: u32, - version_minor: u32, - version_patch: u32, - version_string: *const std::ffi::c_char, -} - impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress { - // Fundamentally in wasm function references and data pointers live in different memory - // spaces so trying to read the "memory" at a function pointer (an index into a table) to - // heuristically determine which API we have (as is done below) is not quite going to work. + // In WebAssembly, function references and data pointers live in different memory spaces, so trying to read the "memory" + // at a function pointer (an index into a table) to heuristically determine which API we have (as is done below) won't work. #[cfg(target_family = "wasm")] fn ensure_static_runtime_compatibility(&self) {} @@ -56,54 +46,59 @@ impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress { // As a result, we can try to interpret the function pointer as a legacy GDExtensionInterface data pointer and check if the // first fields have values version_major=4 and version_minor=0. This might be deep in UB territory, but the alternative is // to not be able to detect Godot 4.0.x at all, and run into UB anyway. - let get_proc_address = self.expect("get_proc_address unexpectedly null"); - let data_ptr = get_proc_address as *const LegacyLayout; // crowbar it via `as` cast - - // Assumption is that we have at least 8 bytes of memory to safely read from (for both the data and the function case). - let major = unsafe { data_ptr.read().version_major }; - let minor = unsafe { data_ptr.read().version_minor }; - let patch = unsafe { data_ptr.read().version_patch }; - if major != 4 || minor != 0 { - // Technically, major should always be 4; loading Godot 3 will crash anyway. - return; + let static_version_str = crate::GdextBuild::godot_static_version_string(); + + // Strictly speaking, this is NOT the type GDExtensionGodotVersion but a 4.0 legacy version of it. They have the exact same + // layout, and due to GDExtension's compatibility promise, the 4.1+ struct won't change; so we can reuse the type. + // We thus read u32 pointers (field by field). + let data_ptr = get_proc_address as *const u32; // crowbar it via `as` cast + + // SAFETY: borderline UB, but on Desktop systems, we should be able to reinterpret function pointers as data. + // On 64-bit systems, a function pointer is typically 8 bytes long, meaning we can interpret 8 bytes of it. + // On 32-bit systems, we can only read the first 4 bytes safely. If that happens to have value 4 (exceedingly unlikely for + // a function pointer), it's likely that it's the actual version and we run 4.0.x. In that case, read 4 more bytes. + let major = unsafe { data_ptr.read() }; + if major == 4 { + // SAFETY: see above. + let minor = unsafe { data_ptr.offset(1).read() }; + if minor == 0 { + // SAFETY: at this point it's reasonably safe to say that we are indeed dealing with that version struct; read the whole. + let data_ptr = get_proc_address as *const sys::GDExtensionGodotVersion; + let runtime_version_str = unsafe { read_version_string(&data_ptr.read()) }; + + panic!( + "gdext was compiled against a newer Godot version: {static_version_str}\n\ + but loaded by legacy Godot binary, with version: {runtime_version_str}\n\ + \n\ + Update your Godot engine version, or read https://godot-rust.github.io/book/toolchain/compatibility.html.\n\ + \n" + ); + } } - let static_version = crate::GdextBuild::godot_static_version_string(); - let runtime_version = unsafe { - let char_ptr = data_ptr.read().version_string; - let c_str = std::ffi::CStr::from_ptr(char_ptr); - - String::from_utf8_lossy(c_str.to_bytes()) - .as_ref() - .strip_prefix("Godot Engine ") - .unwrap_or(&String::from_utf8_lossy(c_str.to_bytes())) - .to_string() - }; - - // Version 4.0.999 is used to signal that we're running Godot 4.1+ but loading extensions in legacy mode. - if patch == 999 { - // Godot 4.1+ loading the extension in legacy mode. - // Note: this can not happen as of June 2023 anymore, because Godot disallows loading 4.0 extensions now. - // TODO(bromeon): a while after 4.1 release, remove this branch. - // - // Instead of panicking, we could *theoretically* fall back to the legacy API at runtime, but then gdext would need to - // always ship two versions of gdextension_interface.h (+ generated code) and would encourage use of the legacy API. - panic!( - "gdext was compiled against a modern Godot version ({static_version}), but loaded in legacy (4.0.x) mode.\n\ - In your .gdextension file, add `compatibility_minimum = 4.1` under the [configuration] section.\n" - ) - } else { - // Truly a Godot 4.0 version. + // From here we can assume Godot 4.1+. We need to make sure that the runtime version is >= static version. + // Lexicographical tuple comparison does that. + let static_version = crate::GdextBuild::godot_static_version_triple(); + let runtime_version_raw = self.runtime_version(); + + // SAFETY: Godot provides this version struct. + let runtime_version = ( + runtime_version_raw.major as u8, + runtime_version_raw.minor as u8, + runtime_version_raw.patch as u8, + ); + + if runtime_version < static_version { + let runtime_version_str = read_version_string(&runtime_version_raw); + panic!( - "gdext was compiled against a newer Godot version ({static_version}),\n\ - but loaded by a legacy Godot binary ({runtime_version}).\n\ - \n\ - Update your Godot engine version.\n\ + "gdext was compiled against newer Godot version: {static_version_str}\n\ + but loaded by older Godot binary, with version: {runtime_version_str}\n\ \n\ - (If you _really_ need an older Godot version, recompile your Rust extension against that one\ - (see `custom-godot` feature). However, that setup will not be supported for a long time.\n\ + Update your Godot engine version, or compile gdext against an older version.\n\ + For more information, read https://godot-rust.github.io/book/toolchain/compatibility.html.\n\ \n" ); } @@ -127,3 +122,16 @@ impl BindingCompat for sys::GDExtensionInterfaceGetProcAddress { unsafe { sys::GDExtensionInterface::load(*self) } } } + +fn read_version_string(version_ptr: &sys::GDExtensionGodotVersion) -> String { + let char_ptr = version_ptr.string; + + // SAFETY: `version_ptr` points to a layout-compatible version struct. + let c_str = unsafe { std::ffi::CStr::from_ptr(char_ptr) }; + + String::from_utf8_lossy(c_str.to_bytes()) + .as_ref() + .strip_prefix("Godot Engine ") + .unwrap_or(&String::from_utf8_lossy(c_str.to_bytes())) + .to_string() +}