You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Revisit use of unsafe in generated code and in libraries #11467
The codegen for the trimmable type map uses unsafe extensively and with the upcoming changes to the unsafe in C# we should make sure our unsafe code matches the new unsafe model.
Source audit and migration analysis
The primary concern is hidden caller obligations, not JNI's unavoidable use of native code. Some APIs expose unchecked native-memory operations through signatures that appear safe today. Other unsafe operations can be replaced with bounded managed APIs. The binding generators are a major migration dependency because their output becomes application and library source.
This analysis is a source-level review of managed runtime code, shared tooling, and generators under src/, build-tools/, and external/Java.Interop/ at 0205e8caae. It is not an exhaustive memory-safety audit. No production code was changed, and no builds, regression tests, crash reproductions, or new-model compilation experiments were run. The findings below are source-audit observations, not confirmed exploits.
Model and adoption constraints
The article describes an opt-in model planned for preview in .NET 11 and production in .NET 12, nominally C# 16. The current language proposal is evolving; the final project switch, syntax details, and BCL annotations must be verified against the compiler/reference assemblies selected for migration.
Under the proposed model:
Member-level unsafe expresses a caller-facing contract; it no longer establishes an unsafe context throughout the implementation. Unsafe operations need explicit interior contexts.
A safe-callable wrapper must discharge its callees' obligations. Adding an interior block without establishing bounds, initialization, ownership, and lifetime does not make it sound.
IntPtr, managed references, and spans can conceal unsafe obligations even without pointer types in their signatures.
Type-level unsafe is removed, and native declarations require explicit safety classification. The current proposal also requires field classification for explicit-layout types.
The compiler does not prove pointer validity, lifetime, ABI correctness, or the truth of safety documentation.
NewString(char[]? text, int length) pins the array and forwards length without checking it against text.Length. JNI receives a pointer, not the managed array's capacity. Reject negative and oversized lengths before calling native code; preserve the existing null behavior deliberately. With those bounds and the pinning scope established, this managed-buffer overload should remain a safe boundary with an interior unsafe block.
All eight primitive-element indexers return ref Elements[index] after checking disposal, but not the index. Pointer indexing has no managed-array bounds check. Add element-count bounds checks in JavaPrimitiveArrays.tt, regenerate, and review the byte-size calculation for overflow.
A returned reference can survive Release() or Dispose(). Checking disposal when acquiring the reference does not protect subsequent use. The existing byref indexer and raw Elements accessor need caller-unsafe lifetime contracts, including prohibition on concurrent release. Add value-returning read/write alternatives that keep storage live throughout each access. Bounds checks alone cannot make an escaping reference safe.
CreateBuiltInDelegate dispatches on delegateType.Name, then uses Unsafe.As<T>(dlg); the template produces 40 such calls. A matching simple name does not prove type identity. Generate type-pattern dispatch or exact type checks with checked casts, preserving the existing fallback for unsupported delegates.
XorLength reinterprets the first byte of a span as ref ulong, relying on an eight-byte extent and alignment assumptions outside its signature. Replace it with bounded BinaryPrimitives reads/writes, preserving the current byte order and hash output. This is not an observed failure with current callers.
Managed strings and a managed span are converted to pointers solely for UTF-8 encoding. Use Encoding.UTF8.GetBytes(ReadOnlySpan<char>, Span<byte>) and destination slices; also make the combined byte-count calculation checked.
Runtime creation accepts raw VM/environment addresses, direct-buffer access exposes raw storage, and the remapping helper scans an IntPtr until a terminator. Document provenance, thread affinity, readable extent, termination, and lifetime as appropriate. A nonzero check cannot establish these properties. Propagate residual obligations at raw entry points, or discharge them through a controlled owner.
Another removal candidate is Crc64Helper.HashCore, which performs pointer indexing and raw ulong loads over managed arrays. A span-based implementation could validate the input range and use bounded reads/table indexing. Do not blindly substitute System.IO.Hashing.Crc64: the repository preserves a different legacy CRC variant and output convention.
The production source/template scan found no <safety> or // SAFETY: documentation and no SkipLocalsInit declarations. The bounded, populated span-based stackalloc and ArrayPool patterns inspected are not, by themselves, findings.
The existing correctness concerns should receive separate linked bug issues and regression tests rather than wait for the .NET 12 migration. They are included here to inform boundary design; this analysis does not assert that they have been reproduced.
Nine-step migration checklist
Fix boundary defects before changing compiler semantics. Address unchecked string lengths, primitive-element bounds/lifetime contracts, and name-based delegate reinterpretation. Add regression coverage for invalid lengths and indices, supported lease/disposal behavior, and an unrelated delegate with a matching simple name. Do not deliberately dereference released storage in an in-process test.
Remove unnecessary unsafe dependencies from tooling. Start with Files.XorLength, scanner UTF-8 encoding, and the legacy CRC implementation. Microsoft.Android.Build.BaseTasks targets netstandard2.0; select APIs supported by its references rather than assuming modern BCL availability. Preserve hash bytes, endianness, and the CRC variant, including offset-based inputs. Remove AllowUnsafeBlocks where a project genuinely no longer needs it.
Migrate generators before broad opt-in.BoundMethod, BoundConstructor, and BoundProperty unconditionally emit member-level unsafety today. Under the new model, that becomes a downstream obligation. Keep ordinary bindings safe-callable where their implementations establish the required invariants; give raw pointer/handle APIs explicit contracts. Update both JNI generators, T4 templates, handwritten helpers, and expected-output fixtures, not only generated .cs files.
Audit overrides and constructors separately. Do not mechanically retain today's unsafe modifiers: an unsafe override cannot add caller obligations to a safe base member. Candidates include primitive-array operations and Crc64.HashCore. Review constructor initializers, derived constructors, and generic construction constraints. Preserve safe public surfaces wherever the implementation owns the safety proof.
Replace blanket unsafe scopes with member and field contracts. Audit RuntimeNativeMethods, JavaMarshalGCBridge, HandleContext, the JNI VM interface, and remapping structures. Use <safety> documentation for residual caller obligations and // SAFETY: notes for local proofs. Document sensitive field invariants, especially HandleContext.controlBlock and JniRemappingLookup.nativeData: provenance/allocator, initialized extent, ownership, synchronization, and lifetime. Mark contract-bearing fields where appropriate under the selected compiler rules.
Classify native declarations and ABI-sensitive fields. Review every DllImport, LibraryImport, and function-pointer invocation against its native declaration before deciding whether obligations are discharged or propagated. Do not classify all native calls uniformly. Apply the selected compiler's explicit-layout field rules to JValue and JniArgumentValue, preserving exact JNI size, offsets, calling conventions, and representations across ABIs.
Add actual new-model compilation coverage.generator-Tests/Integration-Tests/Compiler.cs:109-114 constructs Roslyn compilations directly with allowUnsafe: true; changing an MSBuild project property will not configure those compilations. Add explicit legacy/new-model test modes, compile generated output against real migrated reference assemblies, and test that unsafe APIs fail outside an unsafe context while safe wrappers remain callable. Verify contract metadata survives reference-assembly production and consumption.
Roll out by dependency layer with compatibility gates. Confirm the supported opt-in mechanism for the chosen SDK, then migrate selected tooling, Java.Interop, Mono.Android, and downstream binding fixtures in dependency order. Account for linked source compiled into multiple projects. The shipped binding targets force AllowUnsafeBlocks=true in ExportJarToXml and AddBindingsToCompile; do not advertise binding applications as safe-only or silently enable the new model for all consumers. Test migrated/legacy caller-callee combinations, and recognize that legacy IntPtr APIs remain an annotation blind spot. Keep unsafe permission increases and model opt-outs visible in review.
Audit paths outside C# compiler enforcement.TypeMapAssemblyEmitter emits PE/IL directly, including native registrations. Opting in its own project does not validate emitted IL. Review generated metadata and native-memory operations separately; test opted-in consumers of emitted assemblies, and add on-device coverage for activation, callbacks, registration, GC, and disposal under the affected runtimes/ABIs. Reflection and dynamic invocation also need separate review because the article identifies reflection as outside the initial enforcement scope.
Lifetime and validation notes
Replacing a pointer with a span does not solve lifetime problems when the span aliases releasable native storage. GC.KeepAlive prevents premature collection, not explicit disposal on another thread. Use ownership/ref-counted leases where needed; apply SafeHandle selectively rather than treating thread-affine JNI references as ordinary OS handles.
Validation for implementation work should include the standalone base-task and generator suites, legacy CRC compatibility tests, trimmable-typemap build tests, and the locally built SDK's on-device JNI/runtime tests. Add new-model positive/negative compilation tests in addition to existing runtime tests. None of those validations were executed for this source-only analysis.
changed the title [-][TrimmableTypeMap] Revisit use of `unsafe` in generated code and in libraries[/-][+]Revisit use of `unsafe` in generated code and in libraries[/+]on Oct 8, 2026
The codegen for the trimmable type map uses
unsafeextensively and with the upcoming changes to theunsafein C# we should make sure our unsafe code matches the new unsafe model.Source audit and migration analysis
The primary concern is hidden caller obligations, not JNI's unavoidable use of native code. Some APIs expose unchecked native-memory operations through signatures that appear safe today. Other unsafe operations can be replaced with bounded managed APIs. The binding generators are a major migration dependency because their output becomes application and library source.
This analysis is a source-level review of managed runtime code, shared tooling, and generators under
src/,build-tools/, andexternal/Java.Interop/at 0205e8caae. It is not an exhaustive memory-safety audit. No production code was changed, and no builds, regression tests, crash reproductions, or new-model compilation experiments were run. The findings below are source-audit observations, not confirmed exploits.Model and adoption constraints
The article describes an opt-in model planned for preview in .NET 11 and production in .NET 12, nominally C# 16. The current language proposal is evolving; the final project switch, syntax details, and BCL annotations must be verified against the compiler/reference assemblies selected for migration.
Under the proposed model:
unsafeexpresses a caller-facing contract; it no longer establishes an unsafe context throughout the implementation. Unsafe operations need explicit interior contexts.IntPtr, managed references, and spans can conceal unsafe obligations even without pointer types in their signatures.unsafeis removed, and native declarations require explicit safety classification. The current proposal also requires field classification for explicit-layout types.Upstream tracking: dotnet/runtime#125800.
Findings and removal opportunities
NewString(char[]? text, int length)pins the array and forwardslengthwithout checking it againsttext.Length. JNI receives a pointer, not the managed array's capacity. Reject negative and oversized lengths before calling native code; preserve the existing null behavior deliberately. With those bounds and the pinning scope established, this managed-buffer overload should remain a safe boundary with an interior unsafe block.ref Elements[index]after checking disposal, but not the index. Pointer indexing has no managed-array bounds check. Add element-count bounds checks in JavaPrimitiveArrays.tt, regenerate, and review the byte-size calculation for overflow.Release()orDispose(). Checking disposal when acquiring the reference does not protect subsequent use. The existing byref indexer and rawElementsaccessor need caller-unsafe lifetime contracts, including prohibition on concurrent release. Add value-returning read/write alternatives that keep storage live throughout each access. Bounds checks alone cannot make an escaping reference safe.CreateBuiltInDelegatedispatches ondelegateType.Name, then usesUnsafe.As<T>(dlg); the template produces 40 such calls. A matching simple name does not prove type identity. Generate type-pattern dispatch or exact type checks with checked casts, preserving the existing fallback for unsupported delegates.XorLengthreinterprets the first byte of a span asref ulong, relying on an eight-byte extent and alignment assumptions outside its signature. Replace it with boundedBinaryPrimitivesreads/writes, preserving the current byte order and hash output. This is not an observed failure with current callers.Encoding.UTF8.GetBytes(ReadOnlySpan<char>, Span<byte>)and destination slices; also make the combined byte-count calculation checked.IntPtruntil a terminator. Document provenance, thread affinity, readable extent, termination, and lifetime as appropriate. A nonzero check cannot establish these properties. Propagate residual obligations at raw entry points, or discharge them through a controlled owner.Another removal candidate is Crc64Helper.HashCore, which performs pointer indexing and raw
ulongloads over managed arrays. A span-based implementation could validate the input range and use bounded reads/table indexing. Do not blindly substituteSystem.IO.Hashing.Crc64: the repository preserves a different legacy CRC variant and output convention.The production source/template scan found no
<safety>or// SAFETY:documentation and noSkipLocalsInitdeclarations. The bounded, populated span-basedstackallocandArrayPoolpatterns inspected are not, by themselves, findings.The existing correctness concerns should receive separate linked bug issues and regression tests rather than wait for the .NET 12 migration. They are included here to inform boundary design; this analysis does not assert that they have been reproduced.
Nine-step migration checklist
Fix boundary defects before changing compiler semantics. Address unchecked string lengths, primitive-element bounds/lifetime contracts, and name-based delegate reinterpretation. Add regression coverage for invalid lengths and indices, supported lease/disposal behavior, and an unrelated delegate with a matching simple name. Do not deliberately dereference released storage in an in-process test.
Remove unnecessary unsafe dependencies from tooling. Start with
Files.XorLength, scanner UTF-8 encoding, and the legacy CRC implementation.Microsoft.Android.Build.BaseTaskstargetsnetstandard2.0; select APIs supported by its references rather than assuming modern BCL availability. Preserve hash bytes, endianness, and the CRC variant, including offset-based inputs. RemoveAllowUnsafeBlockswhere a project genuinely no longer needs it.Migrate generators before broad opt-in. BoundMethod, BoundConstructor, and BoundProperty unconditionally emit member-level unsafety today. Under the new model, that becomes a downstream obligation. Keep ordinary bindings safe-callable where their implementations establish the required invariants; give raw pointer/handle APIs explicit contracts. Update both JNI generators, T4 templates, handwritten helpers, and expected-output fixtures, not only generated
.csfiles.Audit overrides and constructors separately. Do not mechanically retain today's
unsafemodifiers: an unsafe override cannot add caller obligations to a safe base member. Candidates include primitive-array operations and Crc64.HashCore. Review constructor initializers, derived constructors, and generic construction constraints. Preserve safe public surfaces wherever the implementation owns the safety proof.Replace blanket unsafe scopes with member and field contracts. Audit
RuntimeNativeMethods,JavaMarshalGCBridge,HandleContext, the JNI VM interface, and remapping structures. Use<safety>documentation for residual caller obligations and// SAFETY:notes for local proofs. Document sensitive field invariants, especiallyHandleContext.controlBlockandJniRemappingLookup.nativeData: provenance/allocator, initialized extent, ownership, synchronization, and lifetime. Mark contract-bearing fields where appropriate under the selected compiler rules.Classify native declarations and ABI-sensitive fields. Review every
DllImport,LibraryImport, and function-pointer invocation against its native declaration before deciding whether obligations are discharged or propagated. Do not classify all native calls uniformly. Apply the selected compiler's explicit-layout field rules toJValueandJniArgumentValue, preserving exact JNI size, offsets, calling conventions, and representations across ABIs.Add actual new-model compilation coverage. generator-Tests/Integration-Tests/Compiler.cs:109-114 constructs Roslyn compilations directly with
allowUnsafe: true; changing an MSBuild project property will not configure those compilations. Add explicit legacy/new-model test modes, compile generated output against real migrated reference assemblies, and test that unsafe APIs fail outside an unsafe context while safe wrappers remain callable. Verify contract metadata survives reference-assembly production and consumption.Roll out by dependency layer with compatibility gates. Confirm the supported opt-in mechanism for the chosen SDK, then migrate selected tooling, Java.Interop, Mono.Android, and downstream binding fixtures in dependency order. Account for linked source compiled into multiple projects. The shipped binding targets force
AllowUnsafeBlocks=truein ExportJarToXml and AddBindingsToCompile; do not advertise binding applications as safe-only or silently enable the new model for all consumers. Test migrated/legacy caller-callee combinations, and recognize that legacyIntPtrAPIs remain an annotation blind spot. Keep unsafe permission increases and model opt-outs visible in review.Audit paths outside C# compiler enforcement. TypeMapAssemblyEmitter emits PE/IL directly, including native registrations. Opting in its own project does not validate emitted IL. Review generated metadata and native-memory operations separately; test opted-in consumers of emitted assemblies, and add on-device coverage for activation, callbacks, registration, GC, and disposal under the affected runtimes/ABIs. Reflection and dynamic invocation also need separate review because the article identifies reflection as outside the initial enforcement scope.
Lifetime and validation notes
Replacing a pointer with a span does not solve lifetime problems when the span aliases releasable native storage.
GC.KeepAliveprevents premature collection, not explicit disposal on another thread. Use ownership/ref-counted leases where needed; applySafeHandleselectively rather than treating thread-affine JNI references as ordinary OS handles.Validation for implementation work should include the standalone base-task and generator suites, legacy CRC compatibility tests, trimmable-typemap build tests, and the locally built SDK's on-device JNI/runtime tests. Add new-model positive/negative compilation tests in addition to existing runtime tests. None of those validations were executed for this source-only analysis.