Skip to content

fix(mir-importer): emit promoted refs as globals - #1726

Open
praveenperera wants to merge 1 commit into
NVIDIA:mainfrom
praveenperera:fix/promoted-const-ref-static
Open

praveenperera wants to merge 1 commit into
NVIDIA:mainfrom
praveenperera:fix/promoted-const-ref-static

Conversation

@praveenperera

@praveenperera praveenperera commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1724

Problem

A reference constant such as const PAIR: &(u32, u32) = &(3, 4), or a borrow rustc promotes to 'static such as &42, was built as an alloca, a store, and the slot's address inside whichever function named it. The reference is 'static, so safe code can return it. An #[inline(never)] device helper that did so handed its caller a pointer into a dead stack frame, and the caller read zeros.

Raw pointers formed from such references had the same problem. On main, const RAW: *mut u32 = &59_u32 as *const u32 as *mut u32 compiles but returns the same dangling stack pointer.

Fix

  • One global per allocation. Each anonymous rustc allocation behind a reference constant becomes one module-level, read-only device global holding rustc's byte image. Every reference into one allocation shares its global, including interior pointers such as &PAIR.1.

  • Stable keys. Each global is keyed by a SHA-256 digest of its contents: alignment, bytes, and relocations resolved to their targets' own keys. Its identity is therefore the same in every build: adding unrelated constants or kernels doesn't rename it, unlike a key based on rustc's interning-ordered AllocId. Two read-only allocations with identical contents share one global, which Rust permits because it does not promise constants distinct addresses. Mutable anonymous memory, such as &mut [..] in a static mut initializer, is never merged.

  • Nested pointers. Pointers stored inside an allocation become initializer relocations to the inner allocation's own global, or to the device static.

  • Aggregate fields and static initializers. The same globals back thin pointer fields of tuple, struct, array, and enum constants, and pointer words in device static initializers (static S: &[u32] = &[1, 2]). Both previously rejected anonymous memory.

  • Typed views. When the importer cannot prove a pointee's device layout byte-identical to rustc's, for example &Option<u32>, the reference declares the same global typed as the pointee. Lowering accepts several types for one immutable initialized global, validates each type against the shared bytes, and keeps one symbol. As a result &Option<u32> and &u32 into its payload still compare equal. An interior view is rejected when its pointee's layout cannot be proven.

  • *mut T into read-only storage. Such a pointer is established as *const T and then cast raw-to-raw, matching the source's as cast. The storage stays read-only.

  • Shared memory. A pointer into a SharedArray or Barrier cannot be stored in a constant or a static. Those pointers are addrspace(3), but every stored pointer targets an initialized global. Two cases are refused at compile time:

    • a constant, constant table, or static initializer that stores a relocation and whose type holds such a pointer;
    • a type-erased stored pointer to a SharedArray or Barrier static.

    The type check is conservative. A value whose type holds a shared-memory pointer is refused whenever it stores any relocated pointer, even if the shared-memory pointer itself is null.

Not changed: a direct &[T; N] constant

A direct &[T; N] reference constant still uses the existing content-keyed promoted-table path. For const A: &[u32; 2] = &[3, 4] and const E: &u32 = &A[0], A and E can therefore get separate globals with the same bytes, and core::ptr::eq(&A[0], E) may be false.

This is allowed. The Rust Reference says constants are inlined wherever they are used, and that references to the same constant are not necessarily guaranteed to refer to the same memory address. Address identity is guaranteed only within a single rustc allocation, and every reference into one allocation shares its global.

Tests

  • promoted_ref_return. Each form above is returned from an #[inline(never)] helper, and the kernel reads back 20 values, including a pointer-identity check. The forms are:

    • named and promoted references;
    • nested references;
    • interior pointers;
    • enum payloads;
    • aggregate fields;
    • Option<&T>;
    • pointers into device statics;
    • static initializers;
    • *mut constants.

    Its verify-code-shape.sh checks that no helper keeps an alloca or a .local depot, and that the constants live in read-only addrspace(1) globals, with one global per shared allocation. It also builds a copy of the example with extra kernels that carry their own constants, and checks that every helper's promoted global keeps the same key.

  • CI coverage. The compile-only lane for libNVVM-verified examples now builds each example normally and runs its verify-code-shape.sh before emit-ltoir, so these checks, and kernel_reference_validity's, run in CI rather than only in GPU smoketests.

  • Negative examples. error_shared_pointer_constant and error_shared_static_relocation pin the two shared-memory diagnostics.

  • Lowering. mir-lower tests cover sharing one global across typed views in both declaration orders, and rejecting a view that does not match its bytes.

Follow-up

The DisjointSlice reference-constant refusal added in #1722 (fixture error_disjoint_slice_const_ref) can be lifted on top of this change.

@copy-pr-bot

copy-pr-bot Bot commented Nov 3, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

praveenperera added a commit to praveenperera/cuda-rust that referenced this pull request Nov 3, 2026
A reference constant such as `const PAIR: &(u32, u32) = &(3, 4)`, or a
borrow rustc promotes to 'static such as `&42`, was materialized as an
alloca, a store, and the slot's address in whichever function named it.
The reference is 'static, so safe code can return it: an
stack frame, and LLVM deleted the store. Raw pointers formed from such
references (`&42 as *const u32 as *mut u32`) had the same problem.

Each anonymous rustc allocation a reference constant points at now
becomes one module-level read-only device global holding rustc's byte
image, keyed by its AllocId so every reference into it, including
interior pointers such as `&PAIR.1`, shares one global. Pointers stored
inside the allocation become relocations to the inner allocation's own
global or to the device static. The same globals back thin pointer
fields of aggregate and enum constants and pointer words in device
static initializers, which previously rejected anonymous memory. A
`*mut T` into that read-only storage is established as `*const T` and
then cast raw-to-raw, as the source's `as` cast does; the storage stays
read-only.

A pointee whose device layout the importer cannot prove byte-identical
to rustc's, such as `&Option<u32>`, declares the same global typed as
the pointee. Lowering now accepts several types for one immutable
initialized global, validates each against the shared bytes, and keeps
one symbol, so `&Option<u32>` and `&u32` into its payload still compare
equal. An interior view is rejected when its pointee's layout cannot
be proven.

A pointer into shared memory cannot be stored in a constant or static.
Every stored pointer targets an initialized global in the global or
constant address space, while a pointer to SharedArray or Barrier is
an addrspace(3) pointer. A constant, constant table, or static
initializer that stores a relocation and whose type holds such a
pointer, through fields, closure captures, or pointees, is refused, as
is a type-erased stored pointer to a SharedArray or Barrier static,
which has no initialized global.

The promoted_ref_return example returns each form from an
device, and verifies that no helper keeps a stack slot or a .local
depot. error_shared_pointer_constant and error_shared_static_relocation
pin the two shared-memory refusals.

Closes NVIDIA#1724

From upstream NVIDIA#1726, which is based on a main without
NVIDIA#1722. Merged here, a reference to a constant that holds a DisjointSlice
is still refused before the new global path, so
error_disjoint_slice_const_ref keeps its diagnostic.

Signed-off-by: Praveen Perera <me@praveenperera.com>
A reference constant such as `const PAIR: &(u32, u32) = &(3, 4)`, or a
borrow rustc promotes to 'static such as `&42`, was materialized as an
alloca, a store, and the slot's address in whichever function named it.
The reference is 'static, so safe code can return it: an
#[inline(never)] device helper handed its caller a pointer into a dead
stack frame, and LLVM deleted the store. Raw pointers formed from such
references (`&42 as *const u32 as *mut u32`) had the same problem.

Each anonymous rustc allocation a reference constant points at now
becomes one module-level read-only device global holding rustc's byte
image. Every reference into it, including interior pointers such as
`&PAIR.1`, shares that global. The global is keyed by a SHA-256 digest
of its contents: alignment, bytes, and relocations resolved to their
targets' own keys. rustc's AllocId numbering follows interning order, so
keying by it renamed a global whenever unrelated constants were added.
Identical read-only allocations share one global, which Rust permits.
Pointers stored inside the allocation become relocations to the inner
allocation's own global or to the device static. The same globals back
thin pointer fields of aggregate and enum constants and pointer words in
device static initializers, which previously rejected anonymous memory.
A `*mut T` into that read-only storage is established as `*const T` and
then cast raw-to-raw, as the source's `as` cast does; the storage stays
read-only.

A pointee whose device layout the importer cannot prove byte-identical
to rustc's, such as `&Option<u32>`, declares the same global typed as
the pointee. Lowering now accepts several types for one immutable
initialized global, validates each against the shared bytes, and keeps
one symbol, so `&Option<u32>` and `&u32` into its payload still compare
equal. An interior view is rejected when its pointee's layout cannot
be proven.

A pointer into shared memory cannot be stored in a constant or static.
Every stored pointer targets an initialized global in the global or
constant address space, while a pointer to SharedArray or Barrier is
an addrspace(3) pointer. A constant, constant table, or static
initializer that stores a relocation and whose type holds such a
pointer, through fields, closure captures, or pointees, is refused, as
is a type-erased stored pointer to a SharedArray or Barrier static,
which has no initialized global.

The promoted_ref_return example returns each form from an
#[inline(never)] helper, checks the values and pointer identity on the
device, and verifies that no helper keeps a stack slot or a .local
depot. error_shared_pointer_constant and error_shared_static_relocation
pin the two shared-memory refusals. The example's check also splices in
kernels with their own constants and requires every helper's global keys
to stay the same. The compile-only smoketest now builds and runs the
shape check of an example in the libNVVM lane before emit-ltoir, so CI
runs it too.

Closes NVIDIA#1724

Signed-off-by: Praveen Perera <me@praveenperera.com>
@praveenperera
praveenperera force-pushed the fix/promoted-const-ref-static branch from f4c42d1 to b4346bc Compare November 3, 2026 20:59
praveenperera added a commit to praveenperera/cuda-rust that referenced this pull request Nov 3, 2026
Bring in the review update to upstream pull request NVIDIA#1726
(head b4346bc). A promoted reference-constant global was keyed by
rustc's AllocId, which follows mono-item order, so adding unrelated
constants or kernels renamed it and changed the PTX of code that had not
changed. An immutable allocation is now keyed by a SHA-256 digest of its
alignment, bytes and relocation targets, so its name stays put and
identical read-only constants share one global. A mutable allocation
keeps its AllocId key. The promoted_ref_return shape check now splices
in extra kernels and requires every helper's globals to be unchanged,
and the compile-only smoketest runs that check.

Keep this branch's module doc diagram, which shows the hashed
__device_global_<crate>_<hash> name lowering emits here.

Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera added a commit to praveenperera/cuda-rust that referenced this pull request Nov 4, 2026
The promoted allocation module doc drew the LLVM global as
@__device_global_0, the counter name lowering used before NVIDIA#1728.
Lowering now names an ordinary device global from a hash of its key,
prefixed by the crate disambiguator when one is set, so the diagram
shows @__device_global_<crate>_<hash>.

Carried over from the fork's 08ac925, which kept this diagram when
it adopted upstream NVIDIA#1726.

Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera added a commit to praveenperera/cuda-rust that referenced this pull request Nov 5, 2026
The promoted allocation module doc drew the LLVM global as
@__device_global_0, the counter name lowering used before NVIDIA#1728.
Lowering now names an ordinary device global from a hash of its key,
prefixed by the crate disambiguator when one is set, so the diagram
shows @__device_global_<crate>_<hash>.

Carried over from the fork's 08ac925, which kept this diagram when
it adopted upstream NVIDIA#1726.

Signed-off-by: Praveen Perera <me@praveenperera.com>

This branch has not been deployed

No deployments
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.

mir-importer: returning a promoted reference constant yields a dangling stack pointer

1 participant