Fix parameter pointer aliasing in class member registration - #54
Open
cobriensr wants to merge 1 commit into
Open
Fix parameter pointer aliasing in class member registration#54cobriensr wants to merge 1 commit into
cobriensr wants to merge 1 commit into
Conversation
When registering a method, virtual method, or signal, the managed parameter info is converted to the unmanaged GDExtensionPropertyInfo, whose name, class_name, and hint_string fields are pointers. The engine only reads those pointers later, when classdb_register_extension_class_method/signal/virtual_method is called at the end of the conversion. The converted values were stored in loop- and block-scoped locals whose addresses (&local) were written into the property info, so their stack slots were reused on the next iteration or after the block closed. Every parameter ended up aliasing the last parameter's name, class name, hint string, and default value, and the return value's pointer fields dangled the same way. Classes with more than one parameter registered with corrupted metadata (all parameters reporting the last one's name, etc.). Convert the parameters into parallel buffers with one slot per parameter, so each property info points at its own stable slot that stays alive until registration. Hoist the return value info and the method name into the registration frame for the same reason. The method and signal paths share the conversion through new internal helpers. The NativeGodotString hint strings created during conversion were also never disposed; the engine copies the data at registration, so dispose them once afterwards. Validate the optional-parameter ordering in an up-front pass, before any native string is created, so an invalid definition throws without leaking the strings converted so far. Add unit tests for the extracted conversion helpers that assert each parameter gets its own slot (pointers pairwise distinct), that the default values match the parameter they belong to (which the aliasing bug collapsed into the last parameter's value), and that the default-value array is compacted onto the trailing optional parameters when they follow a required one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cobriensr
force-pushed
the
fix-param-registration-aliasing
branch
from
July 18, 2026 17:47
08ff45d to
b41e35c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a
[BindMethod], virtual method override, or[Signal]is registered, itsmanaged parameter info is converted to the unmanaged
GDExtensionPropertyInfo,whose
name,class_name, andhint_stringare pointers. The engine readsthose pointers later only when
classdb_register_extension_class_method/…_signal/…_virtual_methodis finally called at the end of the conversion.The converted values were stored in loop- and block-scoped locals, and
their addresses (
&local) were written into the property info:Every iteration reuses the same stack slots, so after the loop all parameters'
args[i].name(andclass_name,hint_string, default value) alias the lastparameter's data.
The return value info had the same defect one level down: its
struct was built inside a nested block, so its pointer fields dangled once that
block closed.
A method/signal with more than one parameter registered with
corrupted per-parameter metadata (all parameters reporting the last one's name,
type hint, and default).
This affected all three registration paths:
BindMethodBindSignalBindVirtualMethod(identical code)Plus a companion leak: the
NativeGodotStringhint strings created during conversion were never disposed.Fix
Convert parameters into parallel buffers with one slot per parameter, so each
GDExtensionPropertyInfopoints at its own stable slot that stays alive untilregistration:
BindMethod/BindVirtualMethodshare a newConvertParameterInfosToNativehelper (per-index
stackallocslots for names, class names, hint strings, anddefault values).
BindSignalgetsConvertSignalParameterInfosToNative. Its native slot typesare
ref structs, so they can't use a managed array; itstackallocs up to theexisting span threshold and falls back to
NativeMemory.Alloc/Free(in afinally) for larger signals.frame for the same lifetime reason.
the data by then).
Virtual-method registration doesn't wire up default arguments, so that behavior
is unchanged as it reuses the shared helper and discards the optional-parameter
count.
Verification
ClassRegistrationContextConversionTests) exercise theextracted helpers with engine-free definitions and assert: (a) each parameter's
pointers reference its own slot and are pairwise distinct; (b) default
values dereference to the value of the parameter they belong to which are the exact
things the aliasing bug corrupted; (c) the default-value array is compacted
onto the trailing optional parameters when they follow a required one (a naive
per-parameter-index write would fail this); and (d) the conversion holds up
above the signal span threshold. Confirmed red/green on both the aliased-slot
pattern and the naive-compaction pattern.
Godot.Bindings.Tests: 205/205 pass (200 baseline + 5).dotnet build src/Godot.Bindings(trim/AOT analyzers on): no new IL or analyzerwarnings as the only ones present are pre-existing and untouched (
IL2075inGodotObject.NativeName.cs,IL2090inMarshalling.cs).Scope / known limitations
StringName/Stringvalues can be constructed without a runningGodot host (creating a non-empty one calls into the engine), so the unit tests
assert on pointer identity/distinctness and on default values built from the
interop-free
bool/int/floatVariant types. There is full end-to-end confirmation nowthat the engine reads back the correct per-parameter names, hints, and
defaults for a registered class. This needs the Godot editor and the integration
harness, which isn't exercised in CI here.
BindPropertyhas a related but lower-confidence variant (a single property, noloop, so the risk is codegen-dependent stack-slot reuse of its block-scoped
locals) plus the same hint-string leak. Left as a follow-up so every change here
is a definite, test-backed fix.
Deliberately left as-is (self-review notes)
A self-review surfaced a few lower-severity items that are intentionally not
changed here, to keep every edit a definite fix:
BindSignalnative-memory (> ParameterSpanThreshold) branch isintegration-only. That branch's
NativeMemory.Alloc/Freeis inseparable fromthe
classdb_register_extension_class_signalcall, which needs a running editor;the added test covers the conversion helper above the threshold, but the heap
alloc/free itself can only be exercised by integration.
strings are disposed after registration; if a conversion or the native register
call were to throw partway, strings created so far would not be disposed. That
path aborts extension loading (fatal) and matches the surrounding code's existing
behavior (e.g.
BindPropertydoesn't dispose its hint string at all). The onereachable trigger, an invalid optional/required parameter ordering is now
validated up front, before any string is created, so it can't leak.
BindSignalkeeps its fixed-sizestack buffers even when it takes the heap path (the standard small-on-stack /
large-on-heap idiom needs an unconditional stack buffer), and
BindVirtualMethodreuses the shared parameter converter, so it passes default-value buffers the
virtual path never reads. Both are negligible and favor sharing the tested helper
over duplicating it.
This was found while auditing the registration path for adoption-blocking bugs. An issue was not
previously filed.