Skip to content

[objcruntime] Reduce duplication for GetCheckedHandle and fix it/them - #14084

Merged
rolfbjarne merged 1 commit into
dotnet:mainfrom
spouliot:getcheckedhandle
Feb 7, 2022
Merged

[objcruntime] Reduce duplication for GetCheckedHandle and fix it/them#14084
rolfbjarne merged 1 commit into
dotnet:mainfrom
spouliot:getcheckedhandle

Conversation

@spouliot

@spouliot spouliot commented Feb 4, 2022

Copy link
Copy Markdown
Contributor

Not sure why it was added to DisposableObject but it seems the compiler
is not happy to use the extension method for the NativeObject subclass
even if it implement INativeObject (that might be the reason why?)

So remove it from NativeObjectExtensions for .net - as I guess it's too
late to remove from the recent DisposableObject in legacy.

The platform assemblies only use it for NativeObject subclass so it's
not an big issue... as I assume there's not many INativeObject user
code that does not use (or should be using) NativeObject.

Fix 1: avoid using IntPtr when NativeHandle is defined since the
compiler will add (not really needed) implicit conversions everywhere.

Fix 2: ensure the spirit/contract of GetCheckedHandle always work.

IOW the goal of GetCheckedHandle is to ensure you never get a nil
handle, since some native API will crash if they given nil

However it's still currently possible, with multithreaded code, to get
a Zero value from GetCheckedHandle.

From a quick look at the C# code it seems fine. It's easier to see the
problem looking at the IL.

.method public hidebysig
	instance valuetype ObjCRuntime.NativeHandle GetCheckedHandle () cil managed
{
	// Method begins at RVA 0x6c8c
	// Header size: 1
	// Code size: 31 (0x1f)
	.maxstack 8

	IL_0000: ldarg.0
	IL_0001: ldfld valuetype ObjCRuntime.NativeHandle ObjCRuntime.DisposableObject::handle
	IL_0006: ldsfld valuetype ObjCRuntime.NativeHandle ObjCRuntime.NativeHandle::Zero
	IL_000b: call bool ObjCRuntime.NativeHandle::op_Equality(valuetype ObjCRuntime.NativeHandle, valuetype ObjCRuntime.NativeHandle)
	IL_0010: brfalse.s IL_0018

	IL_0012: ldarg.0
	IL_0013: call void ObjCRuntime.ThrowHelper::ThrowObjectDisposedException(object)

	IL_0018: ldarg.0
	IL_0019: ldfld valuetype ObjCRuntime.NativeHandle ObjCRuntime.DisposableObject::handle
	IL_001e: ret
} // end of method DisposableObject::GetCheckedHandle

IL_0001 loads the field and then IL_006 compares it to Zero.

If it's not Zero then IL_0019 loads the field again before
returning it IL_0001e to the caller.

So if another thread sets the handle of this instance to Zero between
the two ldfld instructions then the caller will get Zero and this can
crash the application. Of course what will happen if you use the old
handle is somewhat undefined and depends if the native object still
exists. There's a very good chance it does it the managed object i
running (so it has not been collected).

Not sure why it was added to `DisposableObject` but it seems the compiler
is not happy to use the extension method for the `NativeObject` subclass
even if it implement `INativeObject` (that might be the reason why?)

So remove it from `NativeObjectExtensions` for .net - as I guess it's too
late to remove from the recent `DisposableObject` in legacy.

The platform assemblies only use it for `NativeObject` subclass so it's
not an big issue... as I assume there's not many `INativeObject` user
code that does not use (or should be using) `NativeObject`.

Fix #1: avoid using `IntPtr` when `NativeHandle` is defined since the
compiler will add (not really needed) implicit conversions everywhere.

Fix #2: ensure the spirit/contract of `GetCheckedHandle` always work.

IOW the goal of `GetCheckedHandle` is to ensure you never get a `nil`
handle, since some native API will crash if they given `nil`

However it's still currently possible, with multithreaded code, to get
a `Zero` value from `GetCheckedHandle`.

From a quick look at the C# code it seems fine. It's easier to see the
problem looking at the IL.

```il
.method public hidebysig
	instance valuetype ObjCRuntime.NativeHandle GetCheckedHandle () cil managed
{
	// Method begins at RVA 0x6c8c
	// Header size: 1
	// Code size: 31 (0x1f)
	.maxstack 8

	IL_0000: ldarg.0
	IL_0001: ldfld valuetype ObjCRuntime.NativeHandle ObjCRuntime.DisposableObject::handle
	IL_0006: ldsfld valuetype ObjCRuntime.NativeHandle ObjCRuntime.NativeHandle::Zero
	IL_000b: call bool ObjCRuntime.NativeHandle::op_Equality(valuetype ObjCRuntime.NativeHandle, valuetype ObjCRuntime.NativeHandle)
	IL_0010: brfalse.s IL_0018

	IL_0012: ldarg.0
	IL_0013: call void ObjCRuntime.ThrowHelper::ThrowObjectDisposedException(object)

	IL_0018: ldarg.0
	IL_0019: ldfld valuetype ObjCRuntime.NativeHandle ObjCRuntime.DisposableObject::handle
	IL_001e: ret
} // end of method DisposableObject::GetCheckedHandle
```

`IL_0001` loads the field and then `IL_006` compares it to `Zero`.

If it's not `Zero` then `IL_0019` loads the field **again** before
returning it `IL_0001e` to the caller.

So if another thread sets the `handle` of this instance to `Zero` between
the two `ldfld` instructions then the caller will get `Zero` and this can
crash the application. Of course what will happen if you use the _old_
handle is _somewhat_ undefined and depends if the native object still
exists. There's a **very** good chance it does it the managed object i
running (so it has not been collected).
@chamons

chamons commented Feb 4, 2022

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@chamons
chamons requested a review from rolfbjarne February 4, 2022 22:04
@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ [PR Build] Tests passed on Build. ✅

Tests passed on Build.

API diff

✅ API Diff from stable

View API diff
View dotnet API diff
View dotnet legacy API diff
View dotnet iOS-MacCatalayst API diff

API Current PR diff

ℹ️ API Diff (from PR only) (please review changes)

View API diff
View dotnet API diff
View dotnet legacy API diff
View dotnet iOS-MacCatalayst API diff

Generator diff

Generator Diff (no change)

GitHub pages

Results can be found in the following github pages (it might take some time to publish):

🎉 All 108 tests passed 🎉

Pipeline on Agent XAMBOT-1096.BigSur'
Merge 452cbaf into aeabcf3

@rolfbjarne
rolfbjarne merged commit d0c22b8 into dotnet:main Feb 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants