Skip to content

Emit LLVM nonnull / align / dereferenceable for proven Rust references #1098

Open

Description

@uurl

Motivation

Part of #413.

After #1062 preserves Rust pointer/reference kind through dialect MIR and #1089
adds conservatively proven noalias / readonly parameter attributes, the
compiler has the information needed to emit additional LLVM validity attributes
for Rust references.

Rust references carry stronger validity guarantees than raw pointers. In
particular, for sized pointees the compiler can conservatively preserve:

  • non-nullness
  • pointee alignment
  • a dereferenceable byte count when the reference is known to remain valid for
    the required extent

These facts should be derived while the original rustc Ty, layout, and trait
information are still available rather than inferred later from LLVM pointer
shape.

Proposed semantics

Use a conservative subset of rustc's reference parameter policy:

Rust parameter Condition Additional LLVM attributes
&T T: Sized nonnull, align N when N > 1
&mut T T: Sized nonnull, align N when N > 1
&T T: Sized + Freeze, size_of::<T>() > 0 dereferenceable(size_of::<T>())
&mut T T: Sized + Unpin, size_of::<T>() > 0 dereferenceable(size_of::<T>())
unsized reference — no validity attributes in this change
raw pointer — no reference-derived validity attributes

align 1 is omitted as redundant.

Zero-sized pointees never produce dereferenceable(0). An over-aligned ZST may
therefore carry nonnull align N without dereferenceable.

Implementation outline

  1. During MIR import, inspect the original rustc parameter type while type,
    layout, Freeze, and Unpin information is available.
  2. Record only proven per-source-argument validity facts on MirFuncOp.
  3. Verify that reference-derived facts are attached only to source arguments
    represented as Rust references.
  4. Preserve the facts through MIR lowering.
  5. Remap source arguments to physical LLVM pointer parameters after ABI
    flattening.
  6. Make llvm-export mechanically serialize the already-proven attributes.

The downstream layers must not infer Rust validity semantics from pointer
mutability, address space, LLVM pointer shape, or DisjointSlice.

Conservative cases

This change intentionally does not emit validity attributes for:

  • raw pointers
  • erased/compiler-generated pointers
  • unsized references
  • DisjointSlice based solely on its lowered representation

For shared references containing UnsafeCell, nonnull and alignment remain
valid for sized pointees, but dereferenceable is not emitted unless the
required Freeze condition is proven.

For non-Unpin mutable references, nonnull and alignment remain valid for
sized pointees, but dereferenceable is not emitted.

Acceptance criteria

  • sized &T and &mut T emit nonnull
  • pointee alignment greater than one emits LLVM align N
  • &T with Freeze and non-zero pointee size emits
    dereferenceable(size_of::<T>())
  • &mut T with Unpin and non-zero pointee size emits
    dereferenceable(size_of::<T>())
  • &UnsafeCell<T> does not acquire dereferenceable
  • &mut !Unpin does not acquire dereferenceable
  • over-aligned ZST references emit nonnull align N without
    dereferenceable(0)
  • unsized references receive no validity attributes from this policy
  • raw pointers receive no reference-derived validity attributes
  • invalid attribute values/combinations are rejected
  • legacy typed-pointer LLVM export continues to work
  • existing CUDA examples continue to build
  • workspace Clippy passes with warnings denied

Out of scope

  • validity attributes for DST / slice references
  • dereferenceable_or_null
  • recursive provenance analysis
  • deriving validity attributes from DisjointSlice
  • deriving validity attributes from raw pointers
  • general LLVM-level pointer inference

Activity

  1. added
    codegenDevice code-generation pipeline (Rust MIR to IR to PTX)
    DependsBlocked on another PR/issue or an upstream dependency
    IR-loweringLowering between dialects (dialect-mir to LLVM dialect)
    llvm-exportTextual LLVM/NVVM IR exporter (llvm-export crate)
    perfPerformance of generated code or of the compiler itself
    questionFurther information is requested
    soundness-auditCorrectness case under active maintainer audit; not cleared for merge yet
    on Aug 24, 2026
  2. nihalpasham commented on Aug 24, 2026

    @nihalpasham
    Collaborator

    Keeping this open only as part of the same post-#413 design as #1088.

    • nonnull and align may be straightforward, but dereferenceable is an optimizer promise with more conditions than the table in this issue.
    • The closed feat(mir): preserve Rust pointer and reference kind #1062/perf(llvm): emit proven Rust reference parameter attrs #1089 proof path cannot be reused.
    • Re-derive the policy from the pinned rustc ABI implementation, including ZSTs, DSTs, UnsafeCell, pinned types, argument position, optimization mode, and erased reference information.
    • Show a material LLVM/PTX or runtime benefit, with raw-pointer and erased-pointer controls plus verifier tests for invalid combinations.

    No implementation should start until #1088 and #1098 share one auditable per-argument proof model that meets the current acceptance criteria.

  3. nihalpasham commented on Sep 3, 2026

    @nihalpasham
    Collaborator

    This is the half of the #1088 design that can start now. Same rule: every fact comes through rustc_public. No TyCtxt reads, no Debug-string parsing.

    What is buildable today, and what waits:

    attribute              needs                                   status
    nonnull                reference kind (#1186)                  now: SharedRef / UniqueRef from RigidTy::Ref
    align N                pointee layout, N > 1                    now: Ty::layout().shape().abi_align
    dereferenceable(N)     rustc's nofree (Freeze / Unpin facts)   waits for typed ArgAttributes in rustc_public, with #1088's noalias / readonly
    
    • Scope of this half:
      • kernel entry parameters that are Rust references to a sized pointee: &T, &mut T, and the data pointer of &[T] / &mut [T];
      • ZST pointee: nonnull yes, align only when > 1, never dereferenceable(0);
      • raw pointers, DisjointSlice, slice len, by-value structs, extern "C" kernels: nothing.
    • Flow:
      • mir-importer stamps a typed attribute on mir.func per source argument, from kind + layout;
      • mir-lower maps it onto the flattened parameter (slice: data pointer only);
      • llvm-export prints it on kernel entry params, NVVM path only if nvvmVerifyProgram accepts.
    • Expectations: nonnull / align alone will not move PTX much; the measured win (Preserve Rust reference kind and exclusivity through MIR import and lowering #413) arrives with noalias / readonly once the upstream change lands. This half puts the plumbing in place so that becomes a bit copy.
    • Tests: an example that greps its own .ll for the parameter lines: &f32, &mut f32, &[f32], an over-aligned ZST, plus the bare controls above.

    @uurl - want to take this half? An approach note here first would be great.

  4. nihalpasham commented on Sep 3, 2026

    @nihalpasham
    Collaborator

    Filed the upstream ask for typed ArgAttributes in rustc_public: rust-lang/rustc_public#132.

  5. uurl commented on Sep 3, 2026

    @uurl
    ContributorAuthor

    Yes @nihalpasham , I’ll take the nonnull / align half. I’ll post an approach note here first, scoped to facts available through rustc_public only: reference kind + pointee layout, a typed per-source-argument attribute on mir.func, slice-data-pointer remapping in mir-lower, and LLVM export on kernel entry parameters with the bare controls you listed.

    I’ll leave dereferenceable, noalias, and readonly out of scope until typed ArgAttributes are available through rustc_public, and track rust-lang/rustc_public#132 as that dependency.

  6. uurl commented on Sep 4, 2026

    @uurl
    ContributorAuthor

    Thanks — here is the approach I plan to use for the currently unblocked nonnull / align half.

    Proof boundary

    I’ll treat rustc_public as the only semantic source of truth.

    No TyCtxt reads, rustc_internal conversion, auto-trait re-derivation, or Debug parsing.

    The importer-side facts will come from:

    • RigidTy::Ref for reference classification;
    • Ty::layout() for exact pointee alignment.

    For a thin &T / &mut T, the pointee layout directly supplies the alignment.

    For &[T] / &mut [T], [T] itself is unsized, so the slice data-pointer alignment will be derived from the typed element T layout. If the required typed layout is unavailable, the importer will fail closed and emit no validity fact rather than infer one.

    MIR representation

    I plan to add one domain-specific typed MIR attribute representing the proven kernel-parameter validity facts, conceptually:

    ReferenceParamValidity {
        nonnull: true,
        align: Option<N>,
    }
    

    The fact is recorded by source argument index on mir.func.

    I will not reuse the string/integer marker scheme from the closed #1099 path.

    Only facts established by the importer will be transported downstream; neither mir-lower nor llvm-export will infer reference semantics.

    Scope

    This change will apply only to Rust CUDA kernel entry parameters.

    Positive cases:

    • &T → nonnull, plus align N when N > 1
    • &mut T → same
    • &[T] / &mut [T] → attributes on the flattened data pointer only
    • ZST pointee → nonnull; align N only when N > 1

    Bare controls:

    • raw pointers
    • DisjointSlice
    • slice length parameter
    • by-value structs
    • non-kernel/internal functions
    • extern "C" kernel parameters

    No attribute will be recovered from pointer mutability, address space, an erased pointer kind, or lowered LLVM pointer shape.

    Lowering

    mir-lower will consume the typed per-source-argument fact only for a kernel entry function and map it through the existing kernel ABI lowering.

    For slices:

    source &[T]
        |
        +-- LLVM data pointer  -> nonnull / align N
        |
        +-- LLVM length        -> bare
    

    No general-purpose function-parameter attribute inference is planned.

    LLVM export

    llvm-export will mechanically serialize the facts already carried by the lowered function parameter:

    ptr nonnull
    ptr nonnull align N

    No dereferenceable, noalias, or readonly will be emitted in this change.

    The NVVM path will also be checked through the existing verifier/toolkit validation.

    Validation

    I plan to add:

    1. importer tests proving the classification is sourced only from typed rustc_public information;

    2. MIR attribute verifier coverage, including malformed/out-of-range combinations;

    3. source-argument → physical-parameter lowering tests, especially slice data pointer vs. length;

    4. LLVM exporter tests for nonnull and align;

    5. an end-to-end example that inspects its generated .ll parameter lines for:

      • &f32
      • &mut f32
      • &[f32]
      • &mut [f32]
      • an over-aligned ZST
      • the bare controls above;
    6. nvvmVerifyProgram / normal CUDA example validation.

    Explicitly deferred

    These remain blocked on typed ArgAttributes through rustc_public / rust-lang/rustc_public#132:

    • noalias
    • readonly
    • dereferenceable

    I won’t re-derive Freeze, Unpin, NoFree, or equivalent semantics locally.

  7. uurl commented on Sep 4, 2026

    @uurl
    ContributorAuthor

    One soundness edge came up while validating the implementation before opening the PR.

    DeviceBuffer currently represents a zero-byte allocation with CUdeviceptr == 0, and the typed #[cuda_module] slice launch paths forward that pointer into the (data, len) kernel ABI. That conflicts with emitting LLVM nonnull on the data pointer of &[T] / &mut [T] for an empty slice. The same case exists for ZST element buffers, whose byte extent is zero even when len > 0.

    I don't want to paper over that at the exporter boundary.

    My proposed fix is to normalize zero-byte Rust-slice launch pointers at the typed host marshalling boundary: when len == 0 or size_of::<T>() == 0, pass a non-null dangling value aligned to align_of::<T>(); otherwise preserve the actual CUdeviceptr. I would apply this consistently to the sync and async slice launch helpers and add host-side tests for empty slices, over-aligned ZSTs, and the normal non-empty path.

    This does not add dereferenceability or alias promises, and DisjointSlice remains bare.

    If that matches the intended slice contract for this half, I'll include the launch-side normalization with the nonnull / align plumbing.

  8. nihalpasham commented on Sep 6, 2026

    @nihalpasham
    Collaborator

    Looks good to me.

    • nonnull/align scoping matches the split from Emit LLVM noalias / readonly for Rust references with proven semantics #1088: rustc_public-only, typed MIR attribute, kernel entry params only, dereferenceable/noalias/readonly deferred to rustc_public#132.
    • the empty/ZST DeviceBuffer null-pointer issue makes sense - ptr: 0 is intentional there (Drop-skip), and cuda-host's slice launch forwards it as-is today.
    • normalizing at the cuda-host marshalling boundary (not inside DeviceBuffer) is the right layer - that's cuda-oxide's own ABI contract, and not a cutile-rs bug.

    A PR now would be great.

  9. nihalpasham commented on Sep 15, 2026

    @nihalpasham
    Collaborator

    #1244 is ready after maintainer repair and partially addresses this issue.

    • Typed nonnull / alignment facts now reach kernel pointer parameters.
    • The shared host adapter checks nonempty pointer alignment before enqueue; empty/ZST packets use an aligned non-null sentinel.
    • Native and legacy/modern NVVM checks, valid GPU launches, memcheck and hosted CI pass.
    Rust reference fact + checked launch packet -> justified LLVM promise
    

    Keeping #1098 open for dereferenceable and the remaining typed-attribute work; this PR claims no measured speedup.

  10. nihalpasham commented on Oct 11, 2026

    @nihalpasham
    Collaborator

    Related upstream report: LLVM #230960 tracks lost volatile reads with noalias readonly kernel parameters.

    #1483 still adds rustc-proven, no_free-gated sized dereferenceability on top of the existing nonnull/alignment work from #1244. It defers explicit readonly because of the upstream bug. The validity-attribute work can proceed under that narrower contract; this comment does not close the issue.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    DependsBlocked on another PR/issue or an upstream dependencyIR-loweringLowering between dialects (dialect-mir to LLVM dialect)codegenDevice code-generation pipeline (Rust MIR to IR to PTX)llvm-exportTextual LLVM/NVVM IR exporter (llvm-export crate)perfPerformance of generated code or of the compiler itselfquestionFurther information is requestedsoundness-auditCorrectness case under active maintainer audit; not cleared for merge yet

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions