Why is it UB to volatile_write to an address that traps?

std::ptr::write_volatile is pretty flexible about how a volatile write can be used. When not addressing memory that "inside an allocation", it can be used to write to address 0, and do IO and other side-effects if that is the purpose of the addressed location in hardware.

There are two understandable restrictions imposed: the volatile write can't cause changes to other memory within a Rust allocation, and has to be aligned. However there is one restriction that seems hard to justify:

writing to that memory must: not trap

Why not? It is not unusual to have a deliberately crashing function that dereferences a null pointer (e.g. for testing what happens when an application crashes). write_volatile(null_mut(), 0) is only UB if it traps, and whether or not it traps is not known to the compiler as it is platform-specific. How is trapping different to any other platform-specific thing the volatile write might cause, including a e.g. a reboot/system power-down?

1 Like

AFAIK LLVM doesnt provide any method you can write to zero and considers it UB, except inline assembly and null-pointer-is-valid. But I'm not sure about any addresses except 0.

Related: What about: Targets where NULL is a valid pointer · Issue #29 · rust-lang/unsafe-code-guidelines · GitHub


By the way, Rust you can abort on basically any target with an undefined instruction or a wash trap.

There is also that proposal: abort in core · Issue #442 · rust-lang/libs-team · GitHub

LLVM's documentation on volatile says:

The compiler may assume execution will continue after a volatile operation, so operations which modify memory or may have undefined behavior can be hoisted past a volatile operation.

As an exception to the preceding rule, the compiler may not assume execution will continue after a volatile store operation. This restriction is necessary to support the somewhat common pattern in C of intentionally storing to an invalid pointer to crash the program.

So it looks like LLVM support is there for a write_volatile that traps, but not a read_volatile that traps.

The Rust read/write_volatile methods are already documented to accept null pointers where that does not trap - I can't easily see where in the LLVM reference that's justified.

1 Like

Looking at this, you should probably go to https://internals.rust-lang.org/ and post it all there.

This was added recently added:

1 Like

Of course it's known! It's written in the reference!

It's not different. It's the same: compiler may assume that it would be more efficient to do some calculations before doing some store at address 0 and results of these calculations may be lost in both cases. Compiler couldn't do some reorderings, though (e.g. syscalls and other volatile accesses shouldn't be moved).

Yes. And it's responsibility of a developer to ensure that doesn't happen. Not different from any other UB.

I think this has to do with something quite fundamental:

The compiler does not need to know whether something triggers UB to take advantage of it.

If the compiler could only take advantage of UB when it knew it's going to happen, then UB doesn't make sense. If the compiler always knew, it could just tell you in a compiler error.

There are some compiler optimizations that are only OK if the compiler knows for sure that the operation never finishes. For example, moving a write across the volatile write. So if an operation traps, and you have a handler for that trap that never exits, well now the compiler just performed an optimization that was not justified. It's UB to justify such optimizations.

If the trap just results in the program exiting, that's one thing and I don't think that's so bad. But a trap could be handled in all sorts of ways, and volatile ops are only allowed a certain limited set of side effects. If there is a trap handler, it probably performs a side effect that volatile ops are not allowed to have.

4 Likes

This raises an interesting question of what counts as a side effect here. Consider the std case of running on a OS: any memory access can trap into the kernel if the memory is paged out to swap, or not yet loaded in the first place in a file based mmap.

This is supposedly fine, otherwise you couldn't write any software running on an OS. And this can involve a lot of side effects, including network access for a file over NFS. So the solution must be that the kernel is outside the rust AM (abstract machine), or at least a different instance of the AM.

Now, what if you are in a hypothetical kernel that has implemented mmap for it's own address space? Something like a unikernel perhaps where application code and kernel code are running together in the same address space. Would it now be UB to do on demand paging in of files?

I would like to see a clear principled definition of what sort of trap is not acceptable.

1 Like

You would first bring real use-case and, preferably, real project.

Note that relaxation of rules for volatile_read and volatile_write happened not because of theoretical possibility, but because of existence of absolutely concrete chip, AVR's ATtiny1626, that's supported by Rust.

Similarly Rust does a lot of work to help Linux kernel developers—again, because that's concrete kernel with concrete use-cases. Rust developers are much less inclined to support theoretical use-cases that don't have real world use.

I don't think the request is to change the guarantees for volatile accesses, but rather just to write their documentation in less ambiguous language.

To quote Wikipedia on the problems with the word "trap"...

The terms interrupt, trap, exception, fault, and abort are used to distinguish types of interrupts, although "there is no clear consensus as to the exact meaning of these terms". The term trap may refer to any interrupt, to any software interrupt, to any synchronous software interrupt, or only to interrupts caused by instructions with trap in their names. In some usages, the term trap refers specifically to a breakpoint intended to initiate a context switch to a monitor program or debugger. It may also refer to a synchronous interrupt caused by an exceptional condition (e.g., division by zero, invalid memory access, although the term exception is more common for this.

It's actually pretty unambigious:

This use is, in fact, unsound: program is guaranteed to crash in that case (volative reads and writes have to happen), but there are no warranties about internal state of the program observable at this point.

Because that change was done to support very narrow use-case: make access of address 0 that's needed to be accessed in certain circumstances (AVR's ATtiny1626 was explicitly mentioned in the appropriate issue) legal. That's it.

Well… it's true that “trap” is ambigious, but @GKFX have gleaned intent (this wording is there to make something that I want to use illegal) correctly thus it's not clear what do we need to change there.

If there is the need to allow the use of writes to address zero in a way that @GKFX (to trigger exceptions that would be processible inside of Rust program) then we first need a concrete example of why that is a good idea, explicit decision to allow it — then we may change the documentation.