Skip to content

(Arm32) Fix 32-bit Variable Offset Issues with CoreFoundation #1689

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

Closed
wants to merge 2 commits into from
Closed

(Arm32) Fix 32-bit Variable Offset Issues with CoreFoundation #1689

wants to merge 2 commits into from

Conversation

Kaiede
Copy link
Contributor

@Kaiede Kaiede commented Sep 12, 2018

_CFInfo Assumes a 64-bit _cfinfoa type, when it is really 32-bit on 32-bit systems.
_NSCFType assumes a 32-bit _cfinfoa too, making things even more complicated.

This impacts Swift on arm32v7 platforms like Raspberry Pi 3, and has been causing crashes when using Foundation types that bridge between C and Swift, because shared instance variables are offset by 4 bytes (the extra padding). String being a common example.

There's a couple approaches to addressing this:

  1. Back _CFInfo with a UInt that, on paper, matches size with __CFInfoType
  2. Add padding so we can guarantee that _CFInfo can always be 64-bit.

Which one you pick depends on if this is guaranteed to be true: Swift's UInt == CoreFoundation's __CFInfoType

Because Android has some interesting macros in the code that prevented me from answering this question with high confidence, this change picks option 2 which is less risky, even though it does mean an extra 4 bytes for each bridgable object. Option 1 would use less memory on 32-bit systems.

In either case, it makes sense that __NSCFType should be backed by a _CFInfo instead
of a raw int type so that things remain consistent. Share types when possible so we have consistent, understandable behavior. Question is, does __NSCFType even get used anymore? Is it meant to back something else that always uses a 32-bit _cfinfo?

In terms of validation, this was tested on Raspbian Stretch, using armhf architecture (armv7 specifically), using an existing project (http://github.com/Kaiede/RPiLight) that was able to produce the original bug against the swift-4.1.2-RELEASE tag doing a 'swift test' on the package. To validate against swift-4.1.2-RELEASE, I also needed to cherry-pick commit 1086b3b which fixes another 32-bit issue. The swift RC field is 32-bit on arm32, not 64, so that commit is required for 32-bit to work as well.

_CFInfo Assumes a 64-bit _cfinfoa type, when it is really 32-bit on 32-bit systems.
_NSCFType assumes a 32-bit _cfinfoa too, making things even more complicated.

There's a couple approaches to addressing this:
1) Back _CFInfo with a UInt that, on paper, matches size with __CFInfoType
2) Add padding so we can guarantee that _CFInfo can always be 64-bit.

Which one you pick depends on if this is guaranteed to be true: UInt == __CFInfoType

Because Android has some interesting macros in the code, this change picks option 2, even though it
does mean an extra 4 bytes for each bridgable object.

In either case, it makes sense that _NSCFType should be backed by a _CFInfo instead
of a raw int type. Share types when possible so we have consistent, understandable behavior.
@parkera
Copy link
Contributor

parkera commented Sep 12, 2018

This would add 4 bytes to our CFTypes on other 32 bit platforms, so I think we should go with option #1.

It looks like _NSCFType is used by CFRuntime.c, still.

@Kaiede
Copy link
Contributor Author

Kaiede commented Sep 12, 2018

That's fine. As long as someone is willing to buy off on the assertion that UInt on Swift and __CFInfoType in C should be the same size, and there aren't any back doors into getting a 64-bit __CFInfoType, I'm okay with that. I just saw this, which gave me pause:

#if __LP64__ || DEPLOYMENT_TARGET_ANDROID
typedef uint64_t __CFInfoType;
/* ... */
#else
typedef uint32_t __CFInfoType;
/* ... */
#endif

Perhaps my change should include a fix there to align it with the other uses: #if defined(__LP64__) || defined(__LLP64__)

The problem with __NSCFType is that it isn't actually visible to CFRuntime.c, it uses CFRuntimeBase as the proxy. And nothing in the Swift side subclasses from __NSCFType, they insert the _CFInfo directly into the types. The architecture there reads a bit like an unfinished/abandoned thought.

EDIT: No, you are right that CFRuntime does get access to it. Interesting. What's surprising is that this doesn't create problems, though, because CFRuntimeBase/CFInfo/NSCFType should all agree on what _cfinfo looks like.

@Kaiede
Copy link
Contributor Author

Kaiede commented Sep 13, 2018

Seems like I should try to page @johnno1962 on the issue with __CFInfoType, as they checked in the change initially that added DEPLOYMENT_TARGET_ANDROID.

@Kaiede
Copy link
Contributor Author

Kaiede commented Sep 13, 2018

Here's Option 1. May need more changes, I haven't validated it yet. Waiting for my local Arm32 swift build to finish so I can test it against a couple real projects I have affected by this bug.

@millenomi
Copy link
Contributor

In the meantime, let’s see if it regresses anything on our side:

@millenomi
Copy link
Contributor

@swift-ci please test

@Kaiede
Copy link
Contributor Author

Kaiede commented Sep 13, 2018

So, option 1 doesn't actually seem to work, although it's not clear exactly what piece of code is being naughty. In simple cases, alignment/stride of member variables works and matches between C and Swift, and so bridging is okay. But there are other cases where they don't match, which I have yet to find. The result of these cases are heap corruption.

I guess my question is, after so long using 8 bytes (4 byte cfinfo + 4 byte pad, and later an 8 byte cfinfo) which kept alignment clean on 8-byte boundaries (16 bytes on 32-bit, 24 bytes on 32-bit), why is it anathema to effectively revert the change that dropped the padding? The history behind the change to use uint32_t is messy:

  • @johnno1962 Submits 1086b3b which fixes the Swift refcount block on Apr 18 to be a uintptr_t instead of two uint32_ts. That's this year. This doesn't change the size of the struct for 64 bit, but does for 32-bit. Shrinking it from 24 bytes (20 + 4 padding) to 16 bytes. This is because _cfinfoa was 8-byte aligned, which kept the CFRuntimeBase struct 8-byte aligned as a side-effect.
  • @chnmrc Runs across the bug in 4.1.2-RELEASE (https://bugs.swift.org/browse/SR-6812) in early May. Doesn't see the other fix that's in the master branch, and does his own fix that changes _cfinfoa to a uint32_t, but because he's writing it against 4.1.2-RELEASE, it interacts poorly with @johnno1962's change when checked in as a3e72a9. Now CFRuntimeBase is 12 bytes, and no longer 8-byte aligned. It also leaves behind other bugs when applied to 4.1.2, because of a mismatch of offset for _cfinfoa between Swift (which has it at offset 8 on 32-bit) and CFRuntimeBase (which has it at offset 12 on 32-bit). Crashes still ensue in some cases. This incomplete fix when applied to 4.1.2-RELEASE is why I started looking at this.

Here's really what I want to do: Foundation has effectively had an 8-byte aligned CFRuntimeBase since it's origin in this repository. Because of the interaction of these two things, it is no longer 8-byte aligned, and there's problems because of it. Those problems are difficult to track down. (Edit: and likely to only get worse as more focus is poured into 64-bit where this struct is guaranteed to be 8-byte aligned, letting more assumptions about alignment leak into the code without automation running on 32-bit systems to catch them)

While my first commit in the PR isn't ideal, it returns Foundation to the status quo that has been working, and has allowed Swift 3 to run so well on these devices. The change to uint32_t wasn't to save memory, and without resources to track down the side-effects of that change, it seems safer to simply revert it and keep CFRuntimeBase 8-byte aligned.

@johnno1962
Copy link
Contributor

johnno1962 commented Sep 13, 2018

There is a bit of history behind this problem. In Swift 3, the preamble of a Swift Object was 12 bytes on a 32 bit system (isa + two 32 bit refcounts) and the type of _cfinfoa was char[8]. in Swift4 after the CF foundation code was synced _cfinfoa became uint64_t which with 8 byte memory alignment added a new 4 bytes padding to the __CFRuntimeBase structure after the ref counts while in Swift4 preamble became 8 bytes (isa plus 32 bit unified refcount) just before 4.0 was released. This created a problem because while the __CFRuntimeBase structure increased in size the memory allocated for swift instances decreased so the core foundation code was writing off the end of Swift objects when they were bridged and anarchy ensuing. This was fixed by #1525.

This has interacted badly with another subsequent fix (for the same problem?) a3e72a9 that should be reverted if it is still in master as it results in an ungainly size for __CFRuntimeBase of 12 bytes which may end up padded and didn’t update struct _CFInfo on the Swift side. I recommend the type of _cfinfoa always be 64 bits for everything to tie together. I do not recommend merging this PR and back out a3e72a9 instead. You’ll need to check the status of the various Apple branches in particular 4.2.

If you’re looking for a reference point of something that has run the foundation tests and works on 32 bits use This branch which contains a single commit relative to the original 4.1 to get things working.

The DEPLOYMENT_TARGET_ANDROID conditional on __CFInfoType is a red herring that is not visible to Swift, was only added to prevent warnings during the C build and should only affect an Android build.

@Kaiede
Copy link
Contributor Author

Kaiede commented Sep 13, 2018

Yeah, a revert approach would be cleaner. I’ll just do local testing to be sure. :)

@johnno1962
Copy link
Contributor

johnno1962 commented Sep 13, 2018

Sounds good. I’ve just built the 4.2 branch on Android and apart from minor fixes the tests are in good shape. I’ve raised a PR to revert the second incompatible fix on master.

@Kaiede
Copy link
Contributor Author

Kaiede commented Sep 13, 2018

There's some concern that we need a change to fix a more specific issue being caused by __NSCFType using Int32 instead _CFInfo, a couple of us on swift-arm slack are still looking at it. We may still need parts of my PR here that align __NSCFType if that's true.

But it's easy to add that as a separate commit to the revert, we just need to confirm if we need it or not.

@johnno1962
Copy link
Contributor

__NSCFType should probably have a _CFInfo ivar from the look of things.

@Kaiede
Copy link
Contributor Author

Kaiede commented Sep 13, 2018

__NSCFType should probably have a _CFInfo ivar from the look of things.

Yeah, that's one of the things this PR does, so I think we should preserve that change with a separate PR.

And boo, I had a branch just ready to pull when you beat me to it. :(

@Kaiede
Copy link
Contributor Author

Kaiede commented Sep 13, 2018

Closing this one, because of #1693. A new PR will be opened with the __NSCFType part of the change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants