Sound deref of a nullptr

I was playing on the playground as always, and found one interesting feature.

In Rust, it's possible to dereference a null pointer and not get a segfault.

Now can your weak C++ do this??? No??? Rust wins :sunglasses: :sunglasses: :sunglasses:

No, it's Undefined Behavior. UB may do anything, literally. This includes not crashing. UB is not "guaranteed crash/segfault".

You must never dereference a null pointer. Try running it in Miri. It reports:

note: inside `main`
  --> src/main.rs:30:5
   |
30 |     x.deref_null();
   |     ^^^^^^^^^^^^^^

error: Undefined Behavior: dereferencing pointer failed: null pointer is a dangling pointer (it has no provenance)
  --> src/main.rs:17:21
   |
17 |             *self = *(0usize as *const _);
   |                     ^^^^^^^^^^^^^^^^^^^^^ dereferencing pointer failed: null pointer is a dangling pointer (it has no provenance)
   |
   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
   = note: BACKTRACE:
   = note: inside `UnsizedZST::deref_null` at src/main.rs:17:21: 17:42
note: inside `main`
  --> src/main.rs:30:5
   |
30 |     x.deref_null();
8 Likes

Chill out, I just wanted to mention that dereference of a null pointer to a ZST generates no asm :wink:

By the way, do you know why doesn't the compiler actually make UnsizedZST unsized because of the PhantomData<[()]>?

 // SAFETY: Deref of a nullptr to a ZST is being optimized out.

That's not even an optimization -- there's literally nothing to read. Even in debug mode, the LLVM IR just declares the pointer value in debuginfo, then returns.

; playground::UnsizedZST::deref_null
; Function Attrs: inlinehint nonlazybind uwtable
define internal void @_ZN10playground10UnsizedZST10deref_null17h2b88380cf2d7c90dE(ptr align 1 %self) unnamed_addr #0 !dbg !208 {
start:
  %self.dbg.spill = alloca ptr, align 8
  store ptr %self, ptr %self.dbg.spill, align 8
  call void @llvm.dbg.declare(metadata ptr %self.dbg.spill, metadata !213, metadata !DIExpression()), !dbg !214
  ret void, !dbg !215
}
2 Likes

Your program does not generate no assembly because of dereference of a null pointer.

It generates no assembly because it has no main() function.

You should have:

pub fn main() {

instead of:

extern fn main() {

You're not right. I was using #![no_main] to get more readable asm, it's not related to the result of the deref. If you use standard entry with std::rt wrapper, nothing changes.

Well, it does not compile and run when extern is used but does when changed to pub. And then it prints your message ""UnsizedZST cannot be displayed bro".

So the assembler required to do that is generated.

PhantomData is a magical type which is always a zero-sized Sized value, no matter what its generic type is. It can't[1] be defined in user code.


  1. Caveat: you kinda can emulate it with some weird hacks, e.g. the ghost crate, but ghost can't emulate the ?Sized cover. ↩ī¸Ž

2 Likes

It actually compiles and generates

_ZN61_$LT$playground..UnsizedZST$u20$as$u20$core..fmt..Display$GT$3fmt17hf3771de73f49aafaE: # @"_ZN61_$LT$playground..UnsizedZST$u20$as$u20$core..fmt..Display$GT$3fmt17hf3771de73f49aafaE"
# %bb.0:
	sub	rsp, 56
	mov	rdi, rsi
	lea	rax, [rip + .L__unnamed_1]
	mov	qword ptr [rsp + 24], rax
	mov	qword ptr [rsp + 32], 1
	mov	qword ptr [rsp + 8], 0
	lea	rax, [rip + .L__unnamed_2]
	mov	qword ptr [rsp + 40], rax
	mov	qword ptr [rsp + 48], 0
	lea	rsi, [rsp + 8]
	call	qword ptr [rip + _ZN4core3fmt9Formatter9write_fmt17hc0f3e5f7fb738634E@GOTPCREL]
	add	rsp, 56
	ret
                                        # -- End function

main:                                   # @main
# %bb.0:
	sub	rsp, 72
	mov	rax, rsp
	mov	qword ptr [rsp + 8], rax
	mov	rax, qword ptr [rip + _ZN61_$LT$playground..UnsizedZST$u20$as$u20$core..fmt..Display$GT$3fmt17hf3771de73f49aafaE@GOTPCREL]
	mov	qword ptr [rsp + 16], rax
	lea	rax, [rip + .L__unnamed_3]
	mov	qword ptr [rsp + 40], rax
	mov	qword ptr [rsp + 48], 2
	mov	qword ptr [rsp + 24], 0
	lea	rax, [rsp + 8]
	mov	qword ptr [rsp + 56], rax
	mov	qword ptr [rsp + 64], 1
	lea	rdi, [rsp + 24]
	call	qword ptr [rip + _ZN3std2io5stdio6_print17h710d5ba826a1c41eE@GOTPCREL]
	add	rsp, 72
	ret
                                        # -- End function

.L__unnamed_2:

.L__unnamed_4:
	.ascii	"__UnsizedZST cannot be displayed bro__"

.L__unnamed_1:
	.quad	.L__unnamed_4
	.asciz	"&\000\000\000\000\000\000"

.L__unnamed_5:
	.byte	10

.L__unnamed_3:
	.quad	.L__unnamed_2
	.zero	8
	.quad	.L__unnamed_5
	.asciz	"\001\000\000\000\000\000\000"

Hmm, OK. Yes it does.

But at no time have we used the result trying to deference a null pointer. So doing it with no code is an option when UB.

I'm not telling anyone to deref NULLs, no. I just wanted to show some code work of which may be unobvious for some C and C++ coders. This thread was just a joke, I'm sorry if i actually wasted someone's time.

This'd probably be on-topic and appreciated at r/rustjerk. Unless stated otherwise, there's an assumption of genuineness on this forum.

4 Likes

Looks pretty funny, thanks. I don't use Reddit, unfortunately...

OK. No worries. I'm clearly too tired for such a subtle joke.

1 Like