Skip to content

Commit 8733da7

Browse files
authored
Merge pull request #1278 from beicause/safety-level-config
Safeguard levels: fine-tune the amount of runtime validations
2 parents 9cd4d65 + a14de8f commit 8733da7

File tree

33 files changed

+426
-147
lines changed

33 files changed

+426
-147
lines changed

.github/composite/godot-itest/action.yml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ inputs:
5656
default: ''
5757
description: "If provided, acts as an argument for '--target', and results in output files written to ./target/{target}"
5858

59+
rust-target-dir:
60+
required: false
61+
default: ''
62+
description: "If provided, determines where to find the generated dylib. Example: 'release' if Godot editor build would look for debug otherwise."
63+
5964
rust-cache-key:
6065
required: false
6166
default: ''
@@ -151,6 +156,7 @@ runs:
151156
env:
152157
RUSTFLAGS: ${{ inputs.rust-env-rustflags }}
153158
TARGET: ${{ inputs.rust-target }}
159+
OUTPUT_DIR: ${{ inputs.rust-target-dir || 'debug' }}
154160
run: |
155161
targetArgs=""
156162
if [[ -n "$TARGET" ]]; then
@@ -162,8 +168,23 @@ runs:
162168
163169
# Instead of modifying .gdextension, rename the output directory.
164170
if [[ -n "$TARGET" ]]; then
165-
rm -rf target/debug
166-
mv target/$TARGET/debug target
171+
rm -rf target/debug target/release
172+
echo "Build output (tree -L 2 target/$TARGET/$OUTPUT_DIR):"
173+
tree -L 2 target/$TARGET/$OUTPUT_DIR || echo "(tree not installed)"
174+
mv target/$TARGET/$OUTPUT_DIR target/ || {
175+
echo "::error::Output dir $TARGET/$OUTPUT_DIR not found"
176+
exit 1
177+
}
178+
# echo "Intermediate dir (tree -L 2 target):"
179+
# tree -L 2 target || echo "(tree not installed)"
180+
if [[ "$OUTPUT_DIR" != "debug" ]]; then
181+
mv target/$OUTPUT_DIR target/debug
182+
fi
183+
echo "Resulting dir (tree -L 2 target):"
184+
tree -L 2 target || echo "(tree not installed)"
185+
# echo "itest.gdextension contents:"
186+
# cat itest/godot/itest.gdextension
187+
# echo "----------------------------------------------------"
167188
fi
168189
shell: bash
169190

.github/workflows/full-ci.yml

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -378,14 +378,14 @@ jobs:
378378
godot-prebuilt-patch: '4.2.2'
379379
hot-reload: api-4-2
380380

381-
# Memory checks: special Godot binaries compiled with AddressSanitizer/LeakSanitizer to detect UB/leaks.
381+
# Memory checks: special Godot binaries compiled with AddressSanitizer/LeakSanitizer to detect UB/leaks. Always Linux.
382382
# See also https://rustc-dev-guide.rust-lang.org/sanitizers.html.
383383
#
384384
# Additionally, the Godot source is patched to make dlclose() a no-op, as unloading dynamic libraries loses stacktrace and
385385
# cause false positives like println!. See https://github.com/google/sanitizers/issues/89.
386386
#
387387
# There is also a gcc variant besides clang, which is currently not used.
388-
- name: linux-memcheck-nightly
388+
- name: memcheck-nightly
389389
os: ubuntu-22.04
390390
artifact-name: linux-memcheck-nightly
391391
godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san
@@ -395,7 +395,28 @@ jobs:
395395
# Sanitizers can't build proc-macros and build scripts; with --target, cargo ignores RUSTFLAGS for those two.
396396
rust-target: x86_64-unknown-linux-gnu
397397

398-
- name: linux-memcheck-4.2
398+
- name: memcheck-release-disengaged
399+
os: ubuntu-22.04
400+
artifact-name: linux-memcheck-nightly
401+
godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san
402+
rust-toolchain: nightly
403+
rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address
404+
# Currently without itest/codegen-full-experimental.
405+
rust-extra-args: --release --features godot/safeguards-release-disengaged
406+
rust-target: x86_64-unknown-linux-gnu
407+
rust-target-dir: release # We're running Godot debug build with Rust release dylib.
408+
409+
- name: memcheck-dev-balanced
410+
os: ubuntu-22.04
411+
artifact-name: linux-memcheck-nightly
412+
godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san
413+
rust-toolchain: nightly
414+
rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address
415+
# Currently without itest/codegen-full-experimental.
416+
rust-extra-args: --features godot/safeguards-dev-balanced
417+
rust-target: x86_64-unknown-linux-gnu
418+
419+
- name: memcheck-4.2
399420
os: ubuntu-22.04
400421
artifact-name: linux-memcheck-4.2
401422
godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san
@@ -419,6 +440,7 @@ jobs:
419440
rust-toolchain: ${{ matrix.rust-toolchain || 'stable' }}
420441
rust-env-rustflags: ${{ matrix.rust-env-rustflags }}
421442
rust-target: ${{ matrix.rust-target }}
443+
rust-target-dir: ${{ matrix.rust-target-dir }}
422444
rust-cache-key: ${{ matrix.rust-cache-key }}
423445
with-llvm: ${{ contains(matrix.name, 'macos') && contains(matrix.rust-extra-args, 'api-custom') }}
424446
godot-check-header: ${{ matrix.godot-check-header }}
@@ -500,7 +522,7 @@ jobs:
500522
- name: "Determine success or failure"
501523
run: |
502524
DEPENDENCIES='${{ toJson(needs) }}'
503-
525+
504526
echo "Dependency jobs:"
505527
all_success=true
506528
for job in $(echo "$DEPENDENCIES" | jq -r 'keys[]'); do
@@ -510,7 +532,7 @@ jobs:
510532
all_success=false
511533
fi
512534
done
513-
535+
514536
if [[ "$all_success" == "false" ]]; then
515537
echo "One or more dependency jobs failed or were cancelled."
516538
exit 1

.github/workflows/minimal-ci.yml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ jobs:
210210
godot-binary: godot.linuxbsd.editor.dev.x86_64
211211
godot-prebuilt-patch: '4.2.2'
212212

213-
# Memory checkers
213+
# Memory checkers (always Linux).
214214

215-
- name: linux-memcheck
215+
- name: memcheck
216216
os: ubuntu-22.04
217217
artifact-name: linux-memcheck-nightly
218218
godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san
@@ -222,6 +222,27 @@ jobs:
222222
# Sanitizers can't build proc-macros and build scripts; with --target, cargo ignores RUSTFLAGS for those two.
223223
rust-target: x86_64-unknown-linux-gnu
224224

225+
- name: memcheck-release-disengaged
226+
os: ubuntu-22.04
227+
artifact-name: linux-memcheck-nightly
228+
godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san
229+
rust-toolchain: nightly
230+
rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address
231+
# Currently without itest/codegen-full-experimental.
232+
rust-extra-args: --release --features godot/safeguards-release-disengaged
233+
rust-target: x86_64-unknown-linux-gnu
234+
rust-target-dir: release # We're running Godot debug build with Rust release dylib.
235+
236+
- name: memcheck-dev-balanced
237+
os: ubuntu-22.04
238+
artifact-name: linux-memcheck-nightly
239+
godot-binary: godot.linuxbsd.editor.dev.x86_64.llvm.san
240+
rust-toolchain: nightly
241+
rust-env-rustflags: -Zrandomize-layout -Zsanitizer=address
242+
# Currently without itest/codegen-full-experimental.
243+
rust-extra-args: --features godot/safeguards-dev-balanced
244+
rust-target: x86_64-unknown-linux-gnu
245+
225246
steps:
226247
- uses: actions/checkout@v4
227248

@@ -236,6 +257,7 @@ jobs:
236257
rust-toolchain: ${{ matrix.rust-toolchain || 'stable' }}
237258
rust-env-rustflags: ${{ matrix.rust-env-rustflags }}
238259
rust-target: ${{ matrix.rust-target }}
260+
rust-target-dir: ${{ matrix.rust-target-dir }}
239261
with-llvm: ${{ contains(matrix.name, 'macos') && contains(matrix.rust-extra-args, 'api-custom') }}
240262
godot-check-header: ${{ matrix.godot-check-header }}
241263
godot-indirect-json: ${{ matrix.godot-indirect-json }}

godot-bindings/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ api-custom = ["dep:bindgen", "dep:regex", "dep:which"]
3333
api-custom-json = ["dep:nanoserde", "dep:bindgen", "dep:regex", "dep:which"]
3434
api-custom-extheader = []
3535

36+
# Safeguard levels (see godot/lib.rs for detailed documentation).
37+
safeguards-dev-balanced = []
38+
safeguards-release-disengaged = []
39+
3640
[dependencies]
3741
gdextension-api = { workspace = true }
3842

godot-bindings/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// It's the only purpose of this build.rs file. If a better solution is found, this file can be removed.
1111

1212
#[rustfmt::skip]
13-
fn main() {
13+
fn main() {
1414
let mut count = 0;
1515
if cfg!(feature = "api-custom") { count += 1; }
1616
if cfg!(feature = "api-custom-json") { count += 1; }

godot-bindings/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,29 @@ pub fn before_api(major_minor: &str) -> bool {
267267
pub fn since_api(major_minor: &str) -> bool {
268268
!before_api(major_minor)
269269
}
270+
271+
pub fn emit_safeguard_levels() {
272+
// Levels: disengaged (0), balanced (1), strict (2)
273+
let mut safeguards_level = if cfg!(debug_assertions) { 2 } else { 1 };
274+
275+
// Override default level with Cargo feature, in dev/release profiles.
276+
#[cfg(debug_assertions)]
277+
if cfg!(feature = "safeguards-dev-balanced") {
278+
safeguards_level = 1;
279+
}
280+
#[cfg(not(debug_assertions))]
281+
if cfg!(feature = "safeguards-release-disengaged") {
282+
safeguards_level = 0;
283+
}
284+
285+
println!(r#"cargo:rustc-check-cfg=cfg(safeguards_balanced)"#);
286+
println!(r#"cargo:rustc-check-cfg=cfg(safeguards_strict)"#);
287+
288+
// Emit #[cfg]s cumulatively: strict builds get both balanced and strict.
289+
if safeguards_level >= 1 {
290+
println!(r#"cargo:rustc-cfg=safeguards_balanced"#);
291+
}
292+
if safeguards_level >= 2 {
293+
println!(r#"cargo:rustc-cfg=safeguards_strict"#);
294+
}
295+
}

godot-codegen/src/generator/classes.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,14 @@ fn make_class(class: &Class, ctx: &mut Context, view: &ApiView) -> GeneratedClas
192192
let (assoc_memory, assoc_dyn_memory, is_exportable) = make_bounds(class, ctx);
193193

194194
let internal_methods = quote! {
195-
fn __checked_id(&self) -> Option<crate::obj::InstanceId> {
196-
// SAFETY: only Option due to layout-compatibility with RawGd<T>; it is always Some because stored in Gd<T> which is non-null.
197-
let rtti = unsafe { self.rtti.as_ref().unwrap_unchecked() };
198-
let instance_id = rtti.check_type::<Self>();
199-
Some(instance_id)
195+
/// Creates a validated object for FFI boundary crossing.
196+
///
197+
/// Low-level internal method. Validation (liveness/type checks) depend on safeguard level.
198+
fn __validated_obj(&self) -> crate::obj::ValidatedObject {
199+
// SAFETY: Self has the same layout as RawGd<Self> (object_ptr + rtti fields in same order).
200+
let raw_gd = unsafe { std::mem::transmute::<&Self, &crate::obj::RawGd<Self>>(self) };
201+
202+
raw_gd.validated_object()
200203
}
201204

202205
#[doc(hidden)]
@@ -579,10 +582,10 @@ fn make_class_method_definition(
579582

580583
let table_index = ctx.get_table_index(&MethodTableKey::from_class(class, method));
581584

582-
let maybe_instance_id = if method.qualifier() == FnQualifier::Static {
585+
let validated_obj = if method.qualifier() == FnQualifier::Static {
583586
quote! { None }
584587
} else {
585-
quote! { self.__checked_id() }
588+
quote! { Some(self.__validated_obj()) }
586589
};
587590

588591
let fptr_access = if cfg!(feature = "codegen-lazy-fptrs") {
@@ -598,16 +601,14 @@ fn make_class_method_definition(
598601
quote! { fptr_by_index(#table_index) }
599602
};
600603

601-
let object_ptr = &receiver.ffi_arg;
602604
let ptrcall_invocation = quote! {
603605
let method_bind = sys::#get_method_table().#fptr_access;
604606

605607
Signature::<CallParams, CallRet>::out_class_ptrcall(
606608
method_bind,
607609
#rust_class_name,
608610
#rust_method_name,
609-
#object_ptr,
610-
#maybe_instance_id,
611+
#validated_obj,
611612
args,
612613
)
613614
};
@@ -619,8 +620,7 @@ fn make_class_method_definition(
619620
method_bind,
620621
#rust_class_name,
621622
#rust_method_name,
622-
#object_ptr,
623-
#maybe_instance_id,
623+
#validated_obj,
624624
args,
625625
varargs
626626
)

godot-codegen/src/generator/native_structures.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ fn make_native_structure_field_and_accessor(
219219

220220
let obj = #snake_field_name.upcast();
221221

222+
#[cfg(safeguards_balanced)]
222223
assert!(obj.is_instance_valid(), "provided node is dead");
223224

224225
let id = obj.instance_id().to_u64();

godot-core/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ api-4-4 = ["godot-ffi/api-4-4"]
3939
api-4-5 = ["godot-ffi/api-4-5"]
4040
# ]]
4141

42+
# Safeguard levels (see godot/lib.rs for detailed documentation).
43+
safeguards-dev-balanced = ["godot-ffi/safeguards-dev-balanced"]
44+
safeguards-release-disengaged = ["godot-ffi/safeguards-release-disengaged"]
45+
4246
[dependencies]
4347
godot-ffi = { path = "../godot-ffi", version = "=0.4.2" }
4448

godot-core/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ fn main() {
1818

1919
godot_bindings::emit_godot_version_cfg();
2020
godot_bindings::emit_wasm_nothreads_cfg();
21+
godot_bindings::emit_safeguard_levels();
2122
}

0 commit comments

Comments
 (0)