-
Notifications
You must be signed in to change notification settings - Fork 10.5k
implement ManagerBuffer.reallocated to allow realloc'ing the storage #19421
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
Conversation
let newBufferPtr = _stdlib_realloc(oldBufferPtr, newSizeInBytes) | ||
// ref counts: realloc worked: 2 (old ptr is new ptr) | ||
// realloc failed: old ptr: 2, new ptr: 2 | ||
buffer = Unmanaged.fromOpaque(newBufferPtr).takeRetainedValue() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so I think this is just wrong in the case that realloc
fails (ie. old and new buffer have different addresses): assigning to buffer
will then decrement the reference count old the old buffer
(which is already free'd 🔥 --> use after free) and increment the reference count of the new buffer (okay but unnecessary).
could I just write into buffer
's memory without doing any ref count stuff? Then I would also not need to manually adjust them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that ManagedBuffer is a Swift class, you shouldn't need (or want) to do any refcounting, since the refcount is stored in the object and will be copied over. Instead, treat the old and new pointers as opaque pointers and assign like that. You may want to take buffer
as an UnsafeMutablePointer<Buffer>
to have more control over the refcounting. You could probably do it with withUnsafeMutablePointer(to:)
but taking the parameter as a UMP seems like less magic.
This brings up another fun case: if the object has a side table (currently, if it has ever had a weak reference to it, possibly more scenarios in the future) then you'll end up with the side table pointing back to the old location. You'll either need to update it as well, or require (and preferably precondition
) that the object has no side table.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeash Thanks, that sounds like a good plan!
Regarding the side table: Yes, so the initial idea was to change isKnownUniquelyReferenced
to only return true
iff refcount == 1 && no side table. I filed SR-8538 for that. But then @lorentey previously had a use-case for exactly the opposite behaviour SR-5633. Someone then proposed to deprecate isKnownUniquelyReferenced
and introduce two new functions with better names that make it clear what happens with weak/unowned references.
I'm happy to file an SR about that if you think that's a good plan.
For now (ie. this patch) I think it would be good enough to precondition that there's no side-table. Is there a way to do that already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RefCount.h
has a hasSideTable
method, but I don't see anywhere that exposes it to Swift. Your best bet there is probably to add a new runtime function that calls through to that. isKnownUniquelyReferencedSeriouslyEvenIncludingTheSideTableYesIMeanIt
@@ -136,6 +136,38 @@ open class ManagedBuffer<Header, Element> { | |||
} | |||
} | |||
|
|||
public extension ManagedBuffer { | |||
@inline(never) | |||
final class func reallocate<Buffer: ManagedBuffer>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this be underscored?
count: Int, | ||
newMinimumCapacity: Int | ||
) { | ||
precondition(_isBitwiseTakable(Header.self)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we all fine with just preconditioning all of those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's fine. I'd like to also have a higher-level interface that falls back to manual copying for things that aren't bitwise takable, which should be doable if you require the count to be passed in like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that sounds like the best idea to me, will add later
// realloc failed: old ptr: 1, new ptr: 2 | ||
if oldBufferPtr != newBufferPtr { | ||
// in case realloc failed, we need to decrease the reference count once more | ||
Unmanaged<Buffer>.fromOpaque(newBufferPtr).release() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙈
c9f29cf
to
eb911e3
Compare
precondition(_isBitwiseTakable(Header.self)) | ||
precondition(_isBitwiseTakable(Element.self)) | ||
precondition(isKnownUniquelyReferenced(&buffer)) | ||
precondition(!_hasSideTable(&buffer)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeash wired that through now. Hope similar to how you thought.
let newSizeInBytes = MemoryLayout<Header>.stride | ||
+ newMinimumCapacity * MemoryLayout<Element>.stride | ||
|
||
withUnsafePointer(to: &buffer) { bufferPtr in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeash 🙈. Is this really the best way of doing this? I think this is pretty wrong as it doesn't guarantee to give me the right pointer but it does work...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeash checked some more but couldn't find anything that would allow me to rewrite the pointer of inout Buffer
to a new value without messing the with ref counts (apart from the unsafeBitCast obvs)...
eb911e3
to
736b0a5
Compare
test/stdlib/Builtins.swift
Outdated
|
||
tests.test("_hasSideTable") { | ||
var aS1 = X() | ||
expectFalse(_hasSideTable(&aS1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeash are there any other ways for objects to get side-tables besides gaining weak refs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently no, but I believe the side table is intended to expand to serve more needs in the future. For example, it would be the natural place to put stored properties declared in extensions, or other "associated object" like things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeash what about actual associated objects? Isn't that going to require a side table entry?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeash Aren't side tables also generated in case of reference count overflows?
90d5e98
to
6d37aa0
Compare
@swift-ci test this please |
Unfortunately being clever with swift-ci leads to disappointment @swift-ci test |
@jrose-apple haha, I can never remember which bots want what prose. The NIO one only wants 'test this please' I think :) |
@weissi the ones for swift are documented here: https://github.com/apple/swift/blob/master/docs/ContinuousIntegration.md#swift-ci |
6d37aa0
to
6186eea
Compare
rebased to latest master & added @atrick to check that this is along the right lines |
@swift-ci test |
Build failed |
Build failed |
@swift-ci test |
Build failed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code looks good. I think I suggested just lowering to a runtime call in SILGen rather than adding a SIL instruction. But the HasSideTable primitive could be useful in for other features, and we could potentially decide to optimize it away, either by knowing that some types can never have a side table, or by doing some dominance-based analysis. So overall, no objections here.
test/stdlib/Builtins.swift
Outdated
|
||
tests.test("_hasSideTable") { | ||
var aS1 = X() | ||
expectFalse(_hasSideTable(&aS1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeash what about actual associated objects? Isn't that going to require a side table entry?
test/stdlib/Builtins.swift
Outdated
|
||
tests.test("_hasSideTable") { | ||
var aS1 = X() | ||
expectFalse(_hasSideTable(&aS1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mikeash Aren't side tables also generated in case of reference count overflows?
precondition(_isBitwiseTakable(Header.self)) | ||
precondition(_isBitwiseTakable(Element.self)) | ||
precondition(isKnownUniquelyReferenced(&buffer)) | ||
precondition(!_hasSideTable(&buffer)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the only place where hasSideTable is used?
It feels that adding a built-in + instruction for this is the wrong approach. Why not check this in the _swift_stdlib_realloc function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, AFAIK side tables are also created for ref count overflows. So you cannot be sure if the precondition will hold, even if there are no weak references.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took me a while to page this info back in, but the only refcount that overflows currently is the unowned count. If an object has never had weak or unowned references (which I think we can reasonably require for this use case) then it won't have a side table. At least as things stand now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@weissi Any assumptions that we make about this use case at least need to be documented as part of the ManagedBuffer contract. e.g. weak/unowned references are disallowed. Otherwise, we can just get rid of the precondition and call the slow path for _hasSideTable.
@mikeash How are associated objects implemented? Don't they require some kind of side table as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@atrick associated objects go through the objective c runtime (and use a different side table mechanism there because plain swift objects don't use the non-pointer isa).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Relocating an object like this will break associated objects in interesting ways. That's another thing we should be OK to forbid here. I'm not sure if it's practical to assert it, though.
If Swift ever gets something like associated objects that doesn't go through the ObjC runtime, it will likely use the side table mechanism, so that would be another potential source of them in the future.
6186eea
to
55f05d8
Compare
88d1a9d
to
b111f36
Compare
@swift-ci please test |
Build failed |
Build failed |
stdlib/public/SwiftShims/LibcShims.h
Outdated
@@ -70,6 +70,11 @@ __swift_ssize_t _swift_stdlib_write(int fd, const void *buf, __swift_size_t nbyt | |||
SWIFT_RUNTIME_STDLIB_SPI | |||
int _swift_stdlib_close(int fd); | |||
|
|||
static inline void *_swift_stdlib_realloc(void *ptr, __swift_size_t size) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need this now that _swift_reallocObject is a runtime call right?
b111f36
to
58dda8e
Compare
@swift-ci please test |
Build failed |
Build failed |
58dda8e
to
2d9fa20
Compare
@swift-ci please test |
Build failed |
Build failed |
|
@swift-ci please test |
hmm, timed out again. @shahmishal is there some issue? Got two timed out runs in a row at a seemingly random test. |
@swift-ci please merge |
This change makes count and capacity available in type-erased contexts and generates less code for these accessors (just one copy, instead of one per element type). Minor associated change: removed the ability to update capacity, since it's not needed until Swift has a realloc solution (swiftlang/swift#19421, which was reverted simply due to the lack of an evolution proposal!)
This change makes count and capacity available in type-erased contexts and generates less code for these accessors (just one copy, instead of one per element type). Minor associated change: removed the ability to update capacity, since it's not needed until Swift has a realloc solution (swiftlang/swift#19421, which was reverted simply due to the lack of an evolution proposal!)
This change makes count and capacity available in type-erased contexts and generates less code for these accessors (just one copy, instead of one per element type). Minor associated change: removed the ability to update capacity, since it's not needed until Swift has a realloc solution (swiftlang/swift#19421, which was reverted simply due to the lack of an evolution proposal!)
what am I trying to achieve
The end goal of this PR and a bunch of follow-ups is to implement support for
realloc
ating Swift objects (to be used for tail-allocated storage) after confirming with a Swift builtin that the type is 'bit-wise takable'. See also the Swift Forums discussion swift_tailRealloc.what is PR doing?
This pull request adds
tryReallocateUniquelyReferenced
which tries to realloc a uniquely referencedManagedBuffer
_hasSideTable
(stdlib-only) runtime function to check if an object has a side table or not_swift_reallocObject
runtime function which tries to reallocate aHeapObject
if it's safe. Does not work on Darwin right now as we can't efficiently check if it's safe or not