[−][src]Crate guard_trait
This crate provides a guarding mechanism for memory, with an interface that is in some ways
similar to core::pin::Pin
.
Motivation
What this crate attempts to solve, is the problem that data races can occur for memory that is
shared with another process or the kernel (via io_uring
for instance). If the memory is still
shared when the original thread continues to execute after a system call for example, the
original buffer can still be accessed while the system call allows the handler to keep using
the memory. This does not happen with traditional blocking syscalls; the kernel will only
access the memory during the syscall, when the process cannot temporarily do anything else.
However, for more advanced asynchronous interfaces such as io_uring
that never copy memory,
the memory can still be used once the system call has started, which can lead to data races
between two actors sharing memory. To prevent this data race, it is not possible to:
- Read the memory while it is being written to by the kernel, or write to the memory while it is being read by the kernel. This is exactly like Rust's aliasing rules: we can either allow both the kernel and this process to read a buffer, for example in the system call write(2), we can temporarily give the kernel exclusive ownership one or more buffers when the kernel is going to write to them, or we can avoid sharing memory at all with the kernel, but we cannot let either actor have mutable access while the other has any access at all. (aliasing invariant)
- Reclaim the memory while it is being read from or written to by the kernel. This is as simple as it sounds: we simply do not want the buffers to be used for other purposes, either by returning the memory to the heap, where it can be allocated simply so that the kernel can overwrite it when it is not supposed to, or it can corrupt stack variables. (reclamation invariant)
The term "kernel" does not necessarily have to be the other actor that the memory is shared
with; on Redox for example, the io_uring
interface can work solely between regular userspace
processes. Additionally, although being a somewhat niche case, this can also be used for safe
wrappers protecting memory for DMA in device drivers, with a few additional restrictions
(regarding cache coherency) to make that work.
This buffer sharing logic does unfortunately not play very well with the current asynchronous
ecosystem, where almost all I/O is done using regular borrowed slices, and references are
merely borrows which are cancellable at any time, even by leaking. This functions perfectly
when you use synchronous (but non-blocking) system calls where either the process or the
kernel can execute at a time. In contrast, io_uring
is asynchronous, meaning that the
kernel can read and write to buffers, while our program is executing. Therefore, a future
that locally stores an array, aliased by the kernel in io_uring
, cannot stop the kernel from
using the memory again in any reasonable way, if the future were to be Drop
ped, without
blocking indefinitely. What is even worse, is that futures can be leaked at any time, and
arrays allocated on the stack can also be dropped, when the memory is still in use by the
kernel, as a buffer to write data from e.g. a socket. If a (mutable) buffer of a stack is then
used for regular variables... arbitrary program corruption!
What we need in order to solve these two complications, is some way to be able to mark a memory
region as both "borrowed by the kernel" (mutably or immutably), and "undroppable". Since the
Rust borrow checker is smart, any mutable reference with a lifetime that is shorter than
'static
, can trivially be leaked, and the pointer can be used again. This rules out any
reference of lifetime 'a
that 'static
outlives, as those may be used again outside of the
borrow, potentially mutably. Immutable static references are however completely harmless, since
they cannot be dropped nor accessed mutably, and immutable aliasing is always permitted.
Consequently, all buffers that are going to be used in safe code, must be owned. This either
means heap-allocated objects (since we can assume that the heap as a whole has the 'static
lifetime, and allocations stay forever, until deallocated explicitly), buffer pools which
themselves have a guarding mechanism, and static references (both mutable and immutable). We
can however allow borrowed data as well, but because of the semantics around lifetimes, and the
very fact that the compiler has no idea that the kernel is also involved, that requires unsafe
code.
Consider reading "Mental experiments with
io_uring
", and "Notes
on io-uring
" for more information about these
challenges.
Interface
The main type that this crate provides, is the Guarded
struct. Guarded
is a wrapper
that encapsulates any stable pointer (which in safe code, can only be done for types that
implement StableDeref
). The reason for StableDeref
is because the memory must point to
the same buffer, and be independent of the location of the pointer on the stack. Due to this,
a newtype that simply borrows an inner field cannot safely be stored in the Guarded
wrapper, but heap containers such as std::vec::Vec
and std::boxed::Box
implement that
trait.
Once the wrapper is populated with an active guard, which typically happens when an io_uring
opcode is submitted, that will access memory, the wrapper will neither allow merely accessing
the memory, nor dropping it, until the guard successfully returns true
after calling
Guard::try_release
. It is then the responsibility of the guard, to make sure dynamically
that the memory is no longer aliased. There exist a fallible reclamation method, and a method
for trying to access the memory as well, both of which will succeed if the guard is either nonpresent,
or if Guard::try_release
succeeds. The Drop impl will leak the memory entirely (by not
calling the Drop
handler of the inner value), if the guard was not able to release the memory
when the buffer goes out of scope. It is thus highly advised to manually keep track of the
buffer, to prevent accidental leakage.
There are a few corner-cases to these quite strict rules: while they are required for upholding
the reclamation invariant, the no-access restriction is not necessary for usages that only
read memory. Consider a write(2) system call for instance, which will never write to the
buffer, allowing the memory to be accessed immutably while the guard is active, or mutably
once the guard is released. Because of this, there are marker types marker::Shared
and
marker::Exclusive
, as generic parameters to Guarded
. If marker::Shared
is used, the
memory can always be accessed read-only.
Static references are special in the way that they are guaranteed to never be dropped (at least in Safe Rust), and they obviously include things like string literals and other static data. However, a guard is still necessary for mutable references, since they could be written to when they should not, if they were to be moved out from the wrapper.
Furthermore, there are some types that handle their guarding mechanism themselves, unlike the
Guarded
wrapper. The regular Guarded
wrapper assumes that all data comes from the heap,
or some other global location that persists for the duration of the program, but is suboptimal
for custom allocators e.g. in buffer pools. To address this limitation, the Guardable
trait
exists to abstracts the role of what the Guarded
wrapper does, namely being able to insert
a guard, and then protect the memory until the guard can be released. For flexibilility,
prefer impl Guardable
rather than Guarded
, if possible. This trait is implemented by
Guarded
, and the most notable example outside of this crate is BufferSlice
from
redox-buffer-pool
, that needs to
tell the buffer pool (which itself is dynamically allocated) that the pool has guarded memory,
to prevent it from deallocating that that particular slice, or the pool as a whole.
This interface is conceptually roughly analoguous to the std::pin::Pin
API offered by
core
and std
; there is a wrapper Guarded
that encapsulates an pointer, and a trait for
types that can safely be released from the guard. However, there are is also a major
difference: Pin
types do not prevent memory reclamation on the stack by leaking, nor
mutable aliasing, whatsoever, making them unusable for io_uring
. The only thing they
ensure, is that the inner type has to have a stable address (i.e. no moving out) before
dropping. Meanwhile, Guarded
will enforce that one can share an address with another
process or hardware, that has no knowledge of when the buffer is getting reclaimed.
Re-exports
pub extern crate stable_deref_trait; |
Modules
marker | A module for markers, mainly intended to distinguish between shared (read-only) and exclusive (write capable) memory. |
Structs
Guarded | A wrapper for types that can be "guarded", meaning that the memory they point to cannot be safely reclaimed or even moved out, until the guard frees it. For exclusive-access buffers (which are written to, in other words), the data is also completely unaccessible until the guard is released. |
TryUnguardError | The error returned from |
Enums
NoGuard | A no-op guard, that cannot be initialized but still useful in type contexts. This the recommended placeholder for types that do not need guarding. |
Traits
Guard | A trait for guards, that decide whether memory can be deallocated, or whether it may be shared with an actor outside of control from this process at all. |
Guardable | A trait for types that can be "guardable", meaning that they only leak on Drop unless they can remove their guard, that their memory cannot be read from if the kernel may mutate it, and that the memory cannot be written to when the kernel may read it. |
StableDeref | An unsafe marker trait for types that deref to a stable address, even when moved. For example, this is implemented by Box, Vec, Rc, Arc and String, among others. Even when a Box is moved, the underlying storage remains at a fixed location. |