Skip to content

Conversation

@janvorli
Copy link
Member

@janvorli janvorli commented Dec 5, 2025

The NULL_CHECK was checking strictly for NULL, but in some cases, we should be checking for values that are close to null. For example, some interop tests fail because various LDIND variants get pointer that is e.g. 2 or other low value.

The NULL_CHECK was checking strictly for NULL, but in some cases, we should be
checking for values that are close to null. For example, some interop tests
fail because various LDIND variants get pointer that is e.g. 2 or other low
value.
@janvorli janvorli added this to the 11.0.0 milestone Dec 5, 2025
@janvorli janvorli self-assigned this Dec 5, 2025
@janvorli janvorli requested review from BrzVlad and kg as code owners December 5, 2025 22:14
Copilot AI review requested due to automatic review settings December 5, 2025 22:14
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @BrzVlad, @janvorli, @kg
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the interpreter's null pointer checking to handle low pointer values (near-null pointers) correctly. The interpreter was checking strictly for NULL (0), but interop tests were failing when operations like LDIND received pointers with small values like 2, which should also be treated as null references.

Key Changes:

  • Introduces NULL_CHECK_PTR macro that treats addresses below NULL_AREA_SIZE (64KB on Windows, page size on Unix) as null references
  • Replaces NULL_CHECK with NULL_CHECK_PTR for all pointer dereferencing operations in the interpreter
  • Aligns interpreter null checking behavior with the rest of the runtime

#define LOCAL_VAR_ADDR(offset,type) ((type*)(stack + (offset)))
#define LOCAL_VAR(offset,type) (*LOCAL_VAR_ADDR(offset, type))
#define NULL_CHECK(o) do { if ((o) == NULL) { COMPlusThrow(kNullReferenceException); } } while (0)
#define NULL_CHECK_PTR(o) do { if ((TADDR)(o) < NULL_AREA_SIZE) { COMPlusThrow(kNullReferenceException); } } while (0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas, can we align naot NULL_AREA_SIZE with coreclr for Unix:

#define NULL_AREA_SIZE (4*1024)
#define NULL_AREA_SIZE GetOsPageSize()
it was added in dotnet/corert@99841b9.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maxUncheckedOffsetForNullObject used during the code generation must be in sync with NULL_AREA_SIZE used at runtime (there is 2x factor between these two constants).

For correctness, it is ok for maxUncheckedOffsetForNullObject to be less than NULL_AREA_SIZE, but it cannot be more than NULL_AREA_SIZE.

For security, it is best for NULL_AREA_SIZE to be as tight as possible to avoid mischaracterizing dirty segfauls as NullReferenceExceptions unnecessarily.

If we were to unify NULL_AREA_SIZE between NAOT and JIT, it would not be as tight as possible in all situations, and we would be leaving a bit of security on the table.

@jkotas
Copy link
Member

jkotas commented Dec 6, 2025

For example, some interop tests fail because various LDIND variants get pointer that is e.g. 2 or other low value.

Are these tests valid, or are they just testing an unspecified behavior that the JIT happens to have? We do not need to have bug-for-bug compatibility for unspecified behavior.

The check introduced in this PR is a bit more expensive than a simple null check. We do not want to be regressing interpreter executor perf unnecessarily.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants