Skip to content

Fix NullReferenceException for null optional non-blittable struct parameters#1740

Draft
jevansaks wants to merge 1 commit into
mainfrom
user/jevansa/fix-1739-null-optional-nonblittable-struct
Draft

Fix NullReferenceException for null optional non-blittable struct parameters#1740
jevansaks wants to merge 1 commit into
mainfrom
user/jevansa/fix-1739-null-optional-nonblittable-struct

Conversation

@jevansaks

Copy link
Copy Markdown
Member

Summary

Fixes #1739.

Calling an extern with a null optional pointer-to-non-blittable-struct parameter threw a NullReferenceException instead of passing a null pointer. The canonical repro from the issue:

PInvoke.MiniDumpWriteDump(
    process.SafeHandle,
    (uint)Environment.ProcessId,
    dumpStream.SafeFileHandle,
    MINIDUMP_TYPE.MiniDumpWithThreadInfo,
    ExceptionParam: null,
    UserStreamParam: null,
    CallbackParam: null); // -> NullReferenceException

Root cause

MINIDUMP_CALLBACK_INFORMATION is non-blittable in the default AllowMarshaling mode because it contains a delegate field (MINIDUMP_CALLBACK_ROUTINE). A pointer to a managed type is illegal, so CsWin32 fell back to emitting the extern parameter with an in modifier. The friendly overload exposed it as T? and took the null path via ref Unsafe.NullRef<T>(). Because the struct is non-blittable, the P/Invoke marshaler must build a native copy and dereferences the null reference while marshaling it, throwing NullReferenceException.

This affects any optional [In] (not [Out]) parameter that points to a non-blittable struct, not just MiniDumpWriteDump. MiniDumpWriteDump was actually the motivating case for the original optional-pointer feature (#578), but its test only checked the friendly signature shape, never the runtime null behavior.

Fix

Emit such parameters as a T[] array instead of an in parameter — exactly what CsWin32 already does for non-blittable struct fields:

  • A null array marshals to a null pointer.
  • A single-element array marshals to a pointer to the struct.

The friendly overload continues to expose the parameter as a nullable T?, and now forwards param.HasValue ? new[] { param.Value } : null to the extern. The public friendly API is unchanged (still T?); only the internal extern signature changes from in T to T[] for these non-blittable cases.

Generated code before/after (friendly overload argument for CallbackParam):

// before (throws for null):
CallbackParam.HasValue ? &CallbackParamLocal : ref Unsafe.NullRef<...>()
// after:
CallbackParam.HasValue ? new[] { CallbackParam.Value } : null

Blittable optional struct pointers (e.g. ExceptionParam, UserStreamParam) are unaffected and keep the existing pointer projection.

Tests

  • Added runtime tests in GenerationSandbox.Tests that actually call MiniDumpWriteDump:
    • NullOptionalPointerParametersDoNotThrow — the regression: CallbackParam: null now succeeds instead of throwing.
    • CallbackParameterIsMarshaled — passes a real callback and asserts it is invoked, exercising the single-element-array marshaling path.
  • Existing GeneratorTests.MiniDumpWriteDump_AllOptionalPointerParametersAreOptional continues to pass (friendly params are still T?).

Validation

  • Repro project returns 0x1 (success) with CallbackParam: null, no NRE.
  • All 812 non-HighMemory generator tests pass.
  • Everything_NoFriendlyOverloads and the FullMarshaling "Everything" generations pass (all APIs + friendly overloads still compile).

Optional [In] pointer parameters to non-blittable (managed) structs were
emitted with an `in` modifier because a pointer to a managed type is illegal.
The friendly overload then passed `ref Unsafe.NullRef<T>()` on the null path,
which caused the P/Invoke marshaler to dereference a null reference and throw
a NullReferenceException (e.g. MiniDumpWriteDump(..., CallbackParam: null)).

Emit such parameters as a `T[]` array instead, as is already done for
non-blittable struct fields. A null array marshals to a null pointer and a
single-element array marshals to a pointer to the struct. The friendly
overload continues to expose the parameter as a nullable `T?`.

Fixes #1739

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

NullReferenceException from passing null as CallbackParam for MiniDumpWriteDump

1 participant