Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UB: Dangling Pointer, ZSTs. #1433

Open
kuzminrobin opened this issue Dec 6, 2023 · 4 comments
Open

UB: Dangling Pointer, ZSTs. #1433

kuzminrobin opened this issue Dec 6, 2023 · 4 comments

Comments

@kuzminrobin
Copy link

kuzminrobin commented Dec 6, 2023

"16.2. Behavior considered undefined" / "Dangling pointers".
The definition of dangling pointer confuses.

1.

If the size is 0, then the pointer must either point inside of a live allocation (including pointing just after the last byte of the allocation), or it must be directly constructed from a non-zero integer literal.

Which is the right interpretation of the paragraph above?

Interpretation A:
If the size is 0, then in order for the pointer to NOT be dangling the pointer must either point inside of a live allocation (including pointing just after the last byte of the allocation), or it must be directly constructed from a non-zero integer literal.

Interpretation B
{ If the size is 0, then even the dangling pointer must either point inside of a live allocation (including pointing just after the last byte of the allocation), or it must be directly constructed from a non-zero integer literal.
Otherwise, <WHAT?> }

2.

A reference/pointer is "dangling" if it is null or not all of the bytes it points to are part of the same live allocation (so in particular they all have to be part of some allocation).

A. What is the purpose of the fragment "(so in particular they all have to be part of some allocation)"? What will be lost if that fragment is removed?
B. If the pointer points to bytes immediately after the allocation, is that pointer dangling?

3.
Also, (on the same page) in Behavior considered undefined

Evaluating a dereference expression (*expr) on a raw pointer that is dangling or unaligned, even in place expression context (e.g. addr_of!(*expr)).

A. I assume that this paragraph is still applicable to the zero-sized types. In other words, dereferencing a dangling or unaligned zero-sized type raw pointer, is also an undefined behavior. Correct?
B. In the Rustonomicon, 9.11. Final Code, the fragment below (the comments are mine) dereferences the dangling zero-sized type pointer (which is undefined behavior). Correct?

if mem::size_of::<T>() == 0 {  // If it is a zero-sized type (ZST)
    // . . .
    Some(ptr::read(NonNull::<T>::dangling().as_ptr()))   // Dereference a dangling ZST pointer (UB).

4.

Note: Uninitialized memory is also implicitly invalid for any type that has a restricted set of valid values. In other words, the only cases in which reading uninitialized memory is permitted are inside unions and in "padding" (the gaps between the fields/elements of a type).

My interpretation: Reading from uninitialized memory, even by using the correctly aligned zero-sized type pointer pointing to a correct allocation, is undefined behavior.
Is this interpretation correct?


Would be nice to see in the book the clarification of those.

@kuzminrobin kuzminrobin changed the title Dangling Pointer definition confuses UB: Dangling Pointer, ZSTs. Dec 6, 2023
@ChayimFriedman2
Copy link
Contributor

Which is the right interpretation of the paragraph above?

Interpretation A, of course.

If the pointer points to bytes immediately after the allocation, is that pointer dangling?

A pointer is allows to point one past the end of an existing allocation, as long as the memory is never accessed. See e.g. the docs for offset().

I assume that this paragraph is still applicable to the zero-sized types. In other words, dereferencing a dangling or unaligned zero-sized type raw pointer, is also an undefined behavior. Correct?

Yes, they are. In particular, if you cast an integer to a pointer to ZST it is always valid, but if it was part of some allocation and this allocation was freed it is invalid.

In the Rustonomicon, 9.11. Final Code, the fragment below (the comments are mine) dereferences the dangling zero-sized type pointer (which is undefined behavior). Correct?

It is not dangling, as it is constructed from an integer literal for zero sized type, which is valid.

My interpretation: Reading from uninitialized memory, even by using the correctly aligned zero-sized type pointer pointing to a correct allocation, is undefined behavior.
Is this interpretation correct?

I'm not sure if this is guaranteed, but the common understanding is that not. Uninitialized is a property of the bytes, and if there are no bytes there is no uninitialized. The type does not have a restricted set of values, it has one value and that's it.

@kuzminrobin
Copy link
Author

@ChayimFriedman2, thanks for the replies!

Overall would be nice to see all that in the text of the book as a confirmation (hence this bug).


@ChayimFriedman2, I have some extra questions.

5.

A pointer is allows to point one past the end of an existing allocation, as long as the memory is never accessed. See e.g. the docs for offset().

Yes, a pointer is allowed to point one past the end of an existing allocation, that totally makes sense to me (the end() iterators in C++ are implemented with such pointers). But still strictly speaking, based on the Dangling pointers definition I have an impression that

  • for NON-zero-sized type pointers, a pointer pointing past the end of an existing allocation (and not pointing fully to the next existing allocation, i.e. not all the pointed bytes are a part of the same next existing allocation) is dangling;
  • for the zero-sized type pointers, a pointer pointing past the end of an existing allocation is NOT dangling (based on the Dangling pointers definition fragment (see later) complemented with @ChayimFriedman2's clarification that the Interpretation 1.A is correct: "If the size is 0, then in order for the pointer to NOT be dangling the pointer must ... point ... (including pointing just after the last byte of the allocation)").

This has consequence for what causes the undefined behavior from dangling pointers (see occurrences of "dangling" there), and what does not. In particular

  • Evaluating a dereference expression (*expr) on a raw pointer pointing one past the end of an existing allocation
    • is undefined behavior for NON-zero-sized types (because such pointers are dangling),
    • is NOT undefined behavior for zero-sized types (because such pointers are NOT dangling if interpretation 1.A is correct).
  • Creating references and boxes from raw pointer pointing one past the end of an existing allocation
    • is undefined behavior for NON-zero-sized types (because such pointers are dangling),
    • is NOT undefined behavior for zero-sized types (because such pointers are NOT dangling if interpretation 1.A is correct).

@ChayimFriedman2, do you agree with such a conclusion?

6.

It is not dangling, as it is constructed from an integer literal for zero sized type, which is valid.

(I assume, you mean "it is constructed from a non-zero integer literal")
Based on the fragment ptr::read(NonNull::<T>::dangling().as_ptr()) and NonNull::dangling() documentation

  • how did you determine that the pointer "is constructed from a non-zero integer literal",
  • and why do you believe that the pointer is not dangling if the documentation explicitly says: "Creates a new NonNull that is dangling"?

@ChayimFriedman2
Copy link
Contributor

for NON-zero-sized type pointers, a pointer pointing past the end of an existing allocation (and not pointing fully to the next existing allocation, i.e. not all the pointed bytes are a part of the same next existing allocation) is dangling;

Of course dereferencing such pointer is UB: it is out-of-bounds for the allocation.

how did you determine that the pointer "is constructed from a non-zero integer literal",

Because NonNull::dangling() returns a well-aligned ptr, not null and not unaligned.

and why do you believe that the pointer is not dangling if the documentation explicitly says: "Creates a new NonNull that is dangling"?

"Dangling" has multiple meanings; in this context, it means aligned but not points to an existing allocation. For ZSTs, this is enough.

@RalfJung
Copy link
Member

RalfJung commented May 10, 2024

FWIW the rules for zero-sized accesses will become a lot simpler soon: rust-lang/rust#117329.

And yes "dangling" is unfortunately an overloaded term.

Also, some of your questions are entirely unrelated with each other; it's better to open separate issues for them as otherwise the discussion gets all mixed up.

What is the purpose of the fragment "(so in particular they all have to be part of some allocation)"? What will be lost if that fragment is removed?

It is intended as a clarifying remark.

Evaluating a dereference expression (*expr) on a raw pointer that is dangling or unaligned, even in place expression context (e.g. addr_of!(*expr)).

Note that this has since been changed; that text no longer occurs in the reference.

My interpretation: Reading from uninitialized memory, even by using the correctly aligned zero-sized type pointer pointing to a correct allocation, is undefined behavior.

Depends on the ZST. For (), it is well-defined and has been explained above; for ! (or enum Void {}), doing a read is always UB no matter the contents of memory.

Evaluating a dereference expression (*expr) on a raw pointer pointing one past the end of an existing allocation

When #117329 lands, the rules will be always the same, both for zero-sized types and for non-zero-sized types: take the set of memory locations that the pointer points to (it's a set, not a single location! *mut i32 points to 4 bytes), and ensure that all of them are contained in the allocation. The set is empty for zero-sized types, so then things are trivially allowed.

This entire "past the end" business is completely unnecessary in Rust.

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

No branches or pull requests

3 participants