Storing a pointer to an allocator id in an unused object could be a
gadget for escallating priviledge of an attacker, by enabling
use-after-free to corrupt the allocator structure, and then create more
damage.
This commit adds an alternative implementation that does not cache the
allocator id.
The CapPtr refactoring was largely compiler guided; unfortunately, it turns out
that the static-sized alloc function is not well exercised. As such, some type
errors and unnecessary unsafety lurked behind missing template instantiation.
Correct those and add calls to the test harness to make sure we always generate
at least one instance of each small/medium/large case. While here, it doesn't
hurt to make sure that we call all three possible dealloc() flavors as well.
This will, if nothing else, force instantiation of the static-sized dealloc
template as well.
Continue tightening the screws on pointer bounds.
Notably, pointers in remote queues are bounded to the free objects. While we
believe that something like MTE is required to make in-band metadata safe, this
is a kind of defense in depth for StrictProvenance architectures: UAF for small
and medium objects expose mostly other (free) small or medium objects and not
allocator metadata (modulo some potential aliasing when Superslabs and
Mediumslabs interconvert). This might shift the burdon on an attacker from
simply holding a UAF pointer to having had to farm several heap pointers.
The policy of bounding remote queue pointers may make the allocator's behavior
for small objects unexpected: while initial object construction during
allocation (that is, when the free list is empty) continues to cleave out
exportable pointers from elevated pointers to internal slabs, reuse pulls from
free lists of *already-bounded* objects. These objects are queued by the
deallocation side, of course, but these paths now include "parallel
reconstruction" of a pointer to the free object from the amplified view of the
returned pointer, rather than queueing amplified pointers and leaving
reconstruction to the allocation side.
Medium objects are possibly similarly mysterious with the added twist that
medium slabs do not store pointers but rather always cleave from their
self-reference (but their interface has always operated using pointers).
Nevertheless, pointers to medium objects end up in remote queues, so we continue
to engage in "parallel reconstruction" in the deallocation paths.
Mediumslabs are strung on a dllist and used to feed the allocator there. If we
ensure that these (and the root pointer to the list itself) are already
exported, then our alloc paths can bound these to arrive at exposible pointers.
The dealloc paths, where we might want a non-exported pointer, already have one,
as they have gone through amplification to get an arena-bounded pointer.
The sole wrinkle in this plan is that we might need a pointer without platform
constraints to manipulate the memory map for page-based zeroing. Since we have
ample room in the Mediumslab header (a few kilobytes end up being used for
padding; the curious should see b8b5f305 and 3d3b0487), just cache therein a
copy of the CBChunk-bound pointer used in Mediumslab::init() for ::alloc().
Even if we opt not to bound these pointers internally (if they aren't headed out
to the user program or we later derive bounded pointers), they should still be
annotated as something other than CBArena, ensuring that we do not attempt to
use them for general amplification.
Begin turning the screws on bounds: pointers the allocator is about to reveal
must be annotated as CBAllocE. Use the PAL's capptr_export and the AAL's
capptr_bound<> to get them there.
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.
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.
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.
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.
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.
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>
This requires that the caller perform the cast on the output rather than the
input, which is a little closer to the truth. Shuffle some casts into the right
position.
Like alloc_size, this will require amplification internally.
This patch also restores performance to the status quo ante; Clang can once
again see enough to generate the same code as it did before de-static-ing
alloc_size.
We're going to need to amplify the pointer and that's going to require access
to our AddressSpaceManager, which we only get non-statically through our
LargeAlloc.
This patch unto itself makes the world slower, perhaps because Clang can't see
the certainty of aliasing of the static and non-static paths to the same
structure. However, when we also de-static external_pointer, that goes away and
things return to the status quo ante.
Going forward, this gives us explicit pointers with which to carry bounds
annotations. Otherwise, assuming AuthPtr overloads operator->, a OOP-style call
like
AuthPtr<Slab, Bounds> slab;
slab->foo()
will create a `Slab* this` within the body of `Slab::foo`, leaving it unable to
see or propagate the Bounds annotation. If it invokes callees that expect
`AuthPtr` arguments, it will therefore have to fabricate new `Bounds` unsafely.
The link object was previously stored in a disused object. This is
good for reducing meta-data, but if we want to reduce the meta-data
corruption potential, then this is not a good design choice.
This commit moves it into the Metaslab.
Replace the generic check_size() calls with per-{small,medium,large} code paths,
which follow the progression...
* _unchecked: no validation has been performed; check the chunkmap and move
to...
* _checked_chunkmap: the chunkmap indicates that this is the correct slab-type,
and so we have reason to believe that our supposed Allocslab pointers are of
the correct types. Use those pointers to read the sizeclass and move to...
* _checked_sizeclass: additionally, the Allocslab metadata confirms that our
sizeclass matches the expected value. Check that we are at the start of an
object and move to...
* _start: we now believe we have a pointer to the start of a live object. On
a sufficiently capable architecture, we will eventually atomically check that
our pointer is not (scheduled to be) revoked (and/or not reversioned). If
this test passes, our prior pointer arithmetic and loads are justified
(assuming correctness of the quarantine/revocation/reuse) machinery.
The size-less dealloc(void*) path by necessity reads the chunkmap and sizeclass
data itself and so there's no reason to re-validate; as such, it jumps directly
to the _okcmsc point. Similarly, we assume that remote queues are full of
already validated objects, and so the remote handling paths continue to jump
further along even than the _start methods.
MSVC has strong opinions on implicit conversions as used in CI, while Clang both
locally and in CI has weaker opinions. In an effort to avoid subsequent
roundtrips through CI, make clang more strict. Adding -Wconversion definitely
increases the strength of clang's opinions, apparently to include frowning on
some that even MSVC considers OK, so go make explicit the current implicit
behavior.
Confirm that the client has given dealloc() a start-of-object pointer before
that pointer propagates very far (esp., before it enters the remote queues).
As these tests are not free, structure the code so that future interfaces can
skip over them if there's reason to be sure we're at the start of an object.
(On CHERI, for example, one could imagine using sealed pointers.)
For architectures with SSM/MTE, immediately after this test we can update the
memory granule to guard against double-frees; leave TODOs in place.
Squeeze some bits out of allocator IDs so that we can land the sizeclass in
each Remote object. The intent is that, on StrictProvenance architectures like
CHERI, we will be able to route Remote messages through RemoteCache-s without
needing to amplify back to read the sizeclass metadata field out of the slab
headers.