-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Can allocations have gaps? Can they be partially deallocated? Can they grow "in-place"? #430
Comments
There is a really poor alternative way of permitting this while maintaining "no gap" allocations is to say that an attempt to access an out-of-bounds pointer (sometimes?) attempts a It's at least somewhat interesting to observe that the TB retagging rules allow "gapped" tag-provenance-validity even if allocations cannot be gapped. Not particularly relevant, but perhaps still an interesting analog.
For pointers yes, it seems true, but I would like to express that both gapped allocation and partial deallocation imo shouldn't apply to references to I don't know how to encode that into the AM, though, since TB currently just treats reference-to- |
That's actually kind of how Miri implements But as you say in terms of optimizations this at least implies the effects of having gaps and permitting partial deallocation.
This is true for both SB and TB.
I don't understand what you mean by this. References have to be dereferenceable (on fn entry at least), so the part covered by For
It could easily retag them with a new "aliased" state, so this is not the problem. (Unless you mean disallowing partial deallocation of |
That is indeed what I was referring to. It'd essentially need to be an "aliased" protector which makes partial deallocation UB. But having thought about it a bit more, I can't think of any transforms which it would actually matter for (well, at least if we assume "retagging" asserts it's still allocated; is that the case? even though LLVM can't use " |
That is definitely the intent. |
That pattern of merging multiple mmap into one logical allocation would probably also be a problem on CHERI. |
#433 made me think about this again. While I was originally thinking just about I don't want to conflate the separate issues too much, but I am admittedly quite confused at trying to rationalize why my gut instinct is that |
One pattern that I think we definitely have to support (on targets where it works, i.e., not CHERI) is mapping some pages with Given that, it would seem strangely asymmetric to me if they couldn't also shrink. It would also be asymmetric to only allow growing/shrinking at the end, so this should probably work in both directions. The one case that does seem meaningfully different is actually creating a gap in an allocation. I am less sure that people would be expecting that to work, and it is easier to come up with realistic optimizations that would break when there are gaps.
If partial deallocation is a thing, I am strongly opposed to treating full deallocation any different from "partial deallocation where there just happens to be nothing left". I cannot see a justification for introducing such a surprising special case in the semantics. |
This actually sounds quite a bit harder than shrinking allocations, because I'm pretty sure there are optimizations that assume this can't happen. Certainly C++ doesn't allow this, and I suspect there are some LLVM passes that bake in the assumption that the bounds of an allocation can't change over its lifetime. One version that has come up before in regular rust is that a |
|
It's likely we'll want a try_resize that adjusts an allocation in-place without invalidating or copying. This would solve a number of issues with the allocator API, since in many cases it will be a no-op (consider using try_resize to adjust alignment from 4 to 2 -- in almost all allocators it will be a no-op, and this would allow reusing an allocation for small layout mismatches). It also makes excess unnecessary, at least in allocators that can implement it (which admittedly is not all of them). So I would prefer if such an API is semantically possible in the language. |
If they cannot change, then they cannot shrink either. So why would growing be harder than shrinking? Do you have an example of such as pass?
This is UB even without SB/TB because you end up doing cross-allocation arithmetic when indexing into the new slice. I don't see how allocation bounds changing or staying the same is even related to this point.
Realloc is completely different, it logically
So no bounds of existing allocations change with
Yeah that's like a nicely abstracted version of adding or removing pages at the end of a mapping while treating it all as a single allocation. I'd also like to support such an API. Changing only the alignment is very unlikely to cause any problems for anything I think. It's changing the size where things start to become a little dicey. |
There is however the one concern that such an API will likely not be implementable on CHERI. |
On CHERI it would still be possible to implement in cases where the underlying operation is a noop -- For example, in many allocators this would be true for realigning where the start and end alignment are both less than On CHERI it wouldn't be possible to implement this using mmap-style operations, but yeah, it would be legal for an allocator to implement try_resize as always failing, so users should handle the failure regardless. Note that I'm not sure implementing it with mmap operations would be that useful anyway, although you're correct that it is possible. |
According to this, LLVM has a fixed assumption that allocations never change their size. So if Rust wants to allow something like that, a bunch of work on LLVM would have to be done first. |
Rust itself can only create contiguous allocations that can only be deallocated all at once. But does the compiler get to assume that all allocations, including those generated by 3rd party allocators, behave like that?
This does have impact on optimizations:
ptr: *const u8
, if we seeptr.read()
andptr.wrapping_offset(x).read()
, can we conclude that all memory at offset0..x
is dereferenceable?x: &Cell<T>
, then callunknown_fn()
(which might deallocate the memoryx
points to), and then at least one byte of x is read again -- can we conclude that all ofx
(0..size_of::<T>()
) is still dereferenceable?One major example of an API that might violate this is
mmap
. There is a fairly clean way to modelmmap
: we say there is one (exposed) provenance (one allocation) for all mmap'ed memory, and withmmap
/munmap
ranges of memory become dereferenceable with that provenance. But clearly the "allocation" that represents allmmap
ed memory can have gaps, and it can be partially deallocated! If we say that the compiler can assume that allocations never have gaps, then the following is UB:X as *const u8
:This code must be UB because under the "no gap" assumption, the compiler is allowed to insert another read at
ptr.add(PAGE_SIZE)
, but that address is not mapped so we'd get a pagefault.So this leads to some questions:
wrapping_add
is safe, so somehow the 2ndread
is UB? But that memory was mmap'ed!Note that "mmap works in addresses, there is no provenance" does not help: in
break
,ptr
has some provenance, and no matter where it comes from -- if we say "no gaps", no one provenance can be valid for both of these reads with an unmapped page in between them.For the "no partial deallocation" assumption, the situation is similar -- we can also write
mmap
-based code that violates it:X as *const u8
:Is the compiler allowed to insert a speculative
ptr.cast::<[u8; 2*PAGE_SIZE]>().read()
at the end? If yes, this would segfault, so somehow the original code must have UB. I don't think it can reasonably have UB, and therefore we have to accept that partial deallocation is a thing.For allocations growing in-place, if the compiler sees the
malloc
it might exploit that the geometry of the allocation cannot change, so even after calling unknown code an access outside the of the allocation per its initial size could be optimized away as UB -- but if the unknown code was actually growing the allocation to make the access in-bounds, that would be a wrong optimization.The text was updated successfully, but these errors were encountered: