Like all the annotations so far, leave these as CBArena-bounded.
However, we can start to do a little better than we were doing before: for
not-large deallocations, we know that the internal superslab pointer is
sufficiently authoritative that we can use it to get to data structures rather
than also passing an amplified pointer to the allocation in question. This,
then, partially reverts some of the earlier changes made while plumbing
CapPtr<>s through the system.
While here, avoid quite as many void*-s in favor of Remote* or SlabNext*.
Work with high-authority pointers internally, but then copy the address onto to
the low-authority pointer originally given to us (using the same primitive as
amplification, just backwards). This ensures that clients cannot behave
non-monotonically even if they have access to external_pointer(). That is, we
are not an amplification oracle, though we might reveal the address of an object
somewhere clients cannot otherwise access. Notably, we might reveal if a
{Super,Medium,}Slab has changed sizeclass (or been repurposed as or from a
Largealloc), forming a covert channel for data but not capabilities. Of course,
if the pointer provided to external_alloc() is still useful, the implied UAF in
these scenarios is still its own problem.
These capture the primitive architectural operations we are going to use. At
the moment, since all AALs and PALs are not StrictProvenance, the only
implementations are stubs that just subvert the type system (but give us
something to compile against, going forward).
Document and suppress MSVC's confusion with CapPtr<>'s constructor (the
suppression has to happen at the definition, not the use, of the constructor) in
the case of never-needs-initialization Allocator-s.
The test for NoAllocation/AlignedAllocation/neither was broken and this was
undetected until the sandbox demo came into being with its NoAllocation PAL
instance. (Sadly, this remained undetected for so long because we can't
routinely build with C++20 because std::atomic_t<>'s initialization rules
changed in ways that make us require oodles and oodles of RAM at compile time.
This patch has been validated with -j1 on a machine with lots of RAM.)
Separately, it's possible that we end up here without `using namespace std`,
which seems like overkill. So just use `std::size_t` ourselves within the PAL
Concept definition.
Allocate slab is randomly in all possible permutations. This increases
the entropy of the order considerably. This uses an algorithm to build
a random cycle in a slab, and then use this to build the free list.
We disable the per-slab randomisation in the non-CHECK_CLIENT builds.
Like we already do in the size-unknown case. This ensures that in the CHERI
case we'll always bound to the size of the segment we return to the large alloc,
not to a subset. It has the added bonus of ensuring that our bounds are
precisely representable.
We're going to try calling (our, out-of-sandbox) ->dealloc() on pointers into
sandbox memory, so, when CHERIfied, we will need amplification authority over
that memory. Rather than asking the PAL for memory directly, ask the
out-of-sandbox snmalloc so that it will, on CHERI, go through its whole dance
with its AuthMap.
This wrapper will allow us to pass `AuthPtr<T,B> p` to zero() without needing to
write `p.unsafe_auth_ptr` to get to a `T*` inside. Moreover, it will give us a
convenient point to assert that `B` is such that the pointer can be used to
manipulate the memory map (i.e. is not exported).
When we get to CHERI, sizes derived from sizeclasses are always precisely
representable, while other sizes may not be.
This should induce no change in behavior without CHERI, except that we might
PAL::zero slightly more memory this way.
Define various parts of random that can be used to make the layout of
memory more random. Thread this through the allocator.
Expose the concept as part of the Pal. Subsequent commits will expose
that on different platforms.
On CHERI, the compiler will always issue a warning for
`reinterpret_cast<T*>(ptraddr_t)` and similar expressions, and of course, if the
compiler can see far enough into the types, the presence of `if constexpr` will
not save us. Therefore, lift the conditional out to two definitions of
`FreeObject::encode` using `std::enable_if_t` to gate which is used.
When a slab has been fully allocated, then we no longer
check it has entries until something returns an allocation to this slab.
However, it is possible that only a single allocation is available, and
then we can end up frequently on the slow path.
This change only considers free lists that cover at least 1/8 of a slab.
This means that we will hit the slow path less frequently. This also
means that the randomisation changes will have more entropy: with a
single element free list there is only one order.
For large small sizes it can still be a single element, as 1/8 is of the
slab capacity is below 1. We max out the trigger at 31 elements to
reduce unneeded wasted space.
The slab allocation pattern is randomised based on the deallocation
pattern. This achieved by using two queues to enqueue free elements
onto. We pick "randomly", which queue to add to, and then when we take
the free_queue to use, we splice the two queues together.
* Replace time measuring macro
The DO_TIME macro was used originally to get performance numbers. The
macro makes tests hard to debug. This commit replaces it with a proper
C++ class with destructor.
* Bug fix
If the superslab meta data is large, then the calculation for the
sizeclasses that could use the short slab was incorrect. This fixes
that calculation.
Co-authored-by: Nathaniel Wesley Filardo <nfilardo@microsoft.com>
The metaslab contains a field specifying how many elements have been
allocated. As the code has evolved this field has now always become
the maximum capacity of the slab for the sizeclass.
This commit looks up this value based on the sizeclass, and removes the
field from the slab's metadata.
The initial performance monitoring for snmalloc used timing of small
operations to guide the design. This feature has not been maintained or
used for several years.
This commit removes the feature.
This was original designed for Project Snowflake to enable a careful
interoperation between an allocator and the thread suspend behaviour in
.NET.
This feature is not being tested or used by any current project. This
form of interop would be better served by designing a special Pal to
interop with the CLR if this is ever needed.
This commit removes the feature.
The previous reciprocal division branch on the prime that the sizeclass
was constructed from. All sizeclasses can be represented as
2^n * {1,3,5,7}
This lead to a very small table, but some work to calculate the
appropriate shifts and multiplications to implement reciprocal division.
This commit uses a completely uniform representation for every
sizeclass using a lookup table. Due to the precise ranges that we query
the modulus and rounding on, we can do this much more efficiently.
The func-release-rounding exhaustively tests all the queries we are
interested in.
Rather than ::get()-ing the `Superslab` and `Slab` for each object we just
created from the bump pointer, recognize that these objects necessarily come
from the same `Slab` (and so the same `Superslab`). In the eventuality of
CHERI, this means we'll amplify once per bump pointer, not once per created
object.
When post()-ing the RemoteCache to message queues, we push an entire bucket
onto a remote allocator's incoming queue (specifically, the allocator owning
the front Remote in the bucket we're moving). In order to do that, we need to
exceed the bounds of the Remote allocation and reference its Allocslab header
(to get the ->message_queue). On StrictProvenance architectures, this will
require that we amplify the head Remote* and then engage in some pointer math.
While Remotes contain the address of the message_queue as the allocator's
identity, this may not be a pointer, just an address, and may have undergone
obfuscation anyway.
Free list pointers can be exploited by attackers. This commit implements
a simple encoding scheme to detect corruption of the pointers. This can
be used to detect UAF and double free.
This does not currently address anything for Medium or Large
allocations. It also does not address cross thread deallocations.
Co-authored-by: Nathaniel Wesley Filardo <nfilardo@microsoft.com>