It depends a bit on the context. In this case, I suppose youâve found the place where you use vec_arr.as_ptr()
and then mutate through that pointer? For that, thereâs a lengthy note on the methodâs documentation already:
pub const fn as_ptr(&self) -> *const T
Returns a raw pointer to the sliceâs buffer.
The caller must ensure that the slice outlives the pointer this function returns, or else it will end up dangling.
The caller must also ensure that the memory the pointer (non-transitively) points to is never written to (except inside an UnsafeCell
) using this pointer or any pointer derived from it. If you need to mutate the contents of the slice, use as_mut_ptr
.
Modifying the container referenced by this slice may cause its buffer to be reallocated, which would also make any pointers to it invalid.
An additional datapoint can be information from this reference page which calls out more generally as UB:
- Mutating immutable bytes. All bytes reachable through a const-promoted expression are immutable, as well as bytes reachable through borrows in
static
and const
initializers that have been lifetime-extended to 'static
. The bytes owned by an immutable binding or immutable static
are immutable, unless those bytes are part of an UnsafeCell<U>
.Moreover, the bytes pointed to by a shared reference, including transitively through other references (both shared and mutable) and Box
es, are immutable; transitivity includes those references stored in fields of compound types.A mutation is any write of more than 0 bytes which overlaps with any of the relevant bytes (even if that write does not change the memory contents).
And also these 2 sections in the pointer module documentation; this one:
Many functions in this module take raw pointers as arguments and read from or write to them. For this to be safe, these pointers must be valid for the given access. Whether a pointer is valid depends on the operation it is used for (read or write), and the extent of the memory that is accessed (i.e., how many bytes are read/written) â it makes no sense to ask âis this pointer validâ; one has to ask âis this pointer valid for a given accessâ. Most functions use *mut T
and *const T
to access only a single value, in which case the documentation omits the size and implicitly assumes it to be size_of::<T>()
bytes.
The precise rules for validity are not determined yet. The guarantees that are provided at this point are very minimal:
âŚâŚ
and this one:
Pointers are not simply an âintegerâ or âaddressâ. For instance, itâs uncontroversial to say that a Use After Free is clearly Undefined Behavior, even if you âget luckyâ and the freed memory gets reallocated before your read/write (in fact this is the worst-case scenario, UAFs would be much less concerning if this didnât happen!). As another example, consider that wrapping_offset
is documented to ârememberâ the allocated object that the original pointer points to, even if it is offset far outside the memory range occupied by that allocated object. To rationalize claims like this, pointers need to somehow be more than just their addresses: they must have provenance.
A pointer value in Rust semantically contains the following information:
- The address it points to, which can be represented by a
usize
.
- The provenance it has, defining the memory it has permission to access. Provenance can be absent, in which case the pointer does not have permission to access any memory.
The exact structure of provenance is not yet specified, but the permission defined by a pointerâs provenance have a spatial component, a temporal component, and a mutability component:
- Spatial: The set of memory addresses that the pointer is allowed to access.
- Temporal: The timespan during which the pointer is allowed to access those memory addresses.
- Mutability: Whether the pointer may only access the memory for reads, or also access it for writes. Note that this can interact with the other components, e.g. a pointer might permit mutation only for a subset of addresses, or only for a subset of its maximal timespan.
When an allocated object is created, it has a unique Original Pointer. For alloc APIs this is literally the pointer the call returns, and for local variables and statics, this is the name of the variable/static. (This is mildly overloading the term âpointerâ for the sake of brevity/exposition.)
The Original Pointer for an allocated object has provenance that constrains the spatial permissions of this pointer to the memory range of the allocation, and the temporal permissions to the lifetime of the allocation. Provenance is implicitly inherited by all pointers transitively derived from the Original Pointer through operations like offset
, borrowing, and pointer casts. Some operations may shrink the permissions of the derived provenance, limiting how much memory it can access or how long itâs valid for (i.e. borrowing a subfield and subslicing can shrink the spatial component of provenance, and all borrowing can shrink the temporal component of provenance). However, no operation can ever grow the permissions of the derived provenance: even if you âknowâ there is a larger allocation, you canât derive a pointer with a larger provenance. Similarly, you cannot ârecombineâ two contiguous provenances back into one (i.e. with a fn merge(&[T], &[T]) -> &[T]
).
âŚâŚ
are relevant in establishing that the question of âis this particular pointer valid for writesâ is a relevant question to consider in the first place 