No Visible Unmanaged Pointer

ID

csharp.no_visible_unmanaged_pointer

Severity

high

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

CSharp

Tags

api-design, code_smell, memory

Description

Reports a field that holds the address of unmanaged memory — a pointer type such as int* or void*, or one of the IntPtr / UIntPtr / nint / nuint handle types — when the field is externally visible (public, protected or protected internal) and is not readonly.

Visibility is the effective one: the field’s own access level and that of every enclosing type. A public field inside a type that callers in another assembly cannot name is not reachable from there, so it is not reported. That covers the common interop helper — a top-level class with no access modifier is internal — and any member of a nested private or internal type.

readonly fields are not reported: the address is fixed at construction and only the declaring type decides it. const members are not reported either, and private / internal fields are out of scope because every writer lives in code the declaring assembly owns. private protected counts as non-visible, since only derived types inside that assembly reach it. An array of pointers or handles is a reference to a managed array rather than a pointer field, so it is not reported here.

Rationale

An address is not a value the type can validate after the fact. Once the field is writable from outside, any caller in any referencing assembly can point it somewhere else, and from that moment every read, write or free performed through the field acts on the new target. The invariants the type held about that address — what it refers to, how large the block is, who is responsible for releasing it — are all gone, and nothing in the type’s own code can notice.

The failure mode is the unpleasant kind: no exception at the point of misuse, just corruption of unrelated memory, or an address handed to native code that releases it a second time. It also happens outside the managed runtime’s reach, so bounds checking and the garbage collector offer no protection and no diagnostic.

public unsafe class Buffer
{
    public int* Data;                             // FLAW - callers can point it anywhere
    protected IntPtr Handle;                      // FLAW - derived types elsewhere can overwrite it

    public readonly IntPtr FixedHandle;           // OK - set once, by the type itself
    private IntPtr scratch;                       // OK - not visible outside the type
    internal UIntPtr shared;                      // OK - writers confined to this assembly
    public int Length;                            // OK - not an address
}

class InteropHelper                               // no modifier, so internal
{
    public IntPtr Handle;                         // OK - unreachable from another assembly
}

Remediation

Make the field readonly when the address is established once during construction — that is the usual intent and costs nothing. When the address genuinely changes over the object’s lifetime, make the field private and expose the operations callers actually need (read a value at an offset, resize, release), so the type keeps ownership of the address and of the bounds that go with it. For handles that cross into native code, prefer a SafeHandle subclass: it ties the lifetime of the handle to the object and makes release deterministic.