Skip to content

Commit

Permalink
Check runtime and compiled Godot versions for compatibility
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Bromeon committed Nov 30, 2023
1 parent e9177f2 commit a5f13ff
Showing 1 changed file with 61 additions and 54 deletions.
115 changes: 61 additions & 54 deletions godot-ffi/src/compat/compat_4_1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down Expand Up @@ -56,54 +46,58 @@ 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.
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 values.
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"
);
}
Expand All @@ -127,3 +121,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()
}

0 comments on commit a5f13ff

Please sign in to comment.