Skip to content

Commit 184488d

Browse files
authored
Conditionalize the fix for SR-14635 (#40822)
* Refactor Bincompat Organize everything around internal functions that test for a particular OS version. Correctly handle cases where we don't know the version of the app. Make all bincompat functions consistently return `true` for the legacy semantics, `false` for new semantics. Consistently name them all to reflect this. * Conditionalize the support for SR-14635 SR-14635 pointed out a hole in the updated dynamic casting logic that allowed certain casts that should have been illegal. In particular, when casting certain types to Obj-C protocols, the Swift value gets boxed; we would permit the cast to succeed whenever the resulting box satisfied the protocol. For example, this allowed any Swift value to be cast to `NSCopying` regardless of whether or not it implemented the required `copy(with:)` method. This was fixed in #37683 to reject such casts but of course some folks were depending on this behavior to pass Swift data into Obj-C functions. (The properly supported approach for passing arbitrary Swift data into Obj-C functions is to cast the Swift value to `AnyObject`.) This change makes that new behavior conditional. For now, the legacy semantics are enabled on Apple platforms and the new semantics are in use everywhere else. This will allow us to gradually enable enforcement of the new behavior over time. * Just skip this test on Apple platforms, since it is inconsistently implemented there (and is therefore not really testable)
1 parent ad7f9a7 commit 184488d

File tree

5 files changed

+123
-57
lines changed

5 files changed

+123
-57
lines changed

include/swift/Runtime/Bincompat.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,20 @@ namespace bincompat {
2323
/// Whether protocol conformance iteration should be reversed, to prefer
2424
/// conformances from images that are later in the list over earlier ones.
2525
/// Default is false starting with Swift 5.4.
26-
bool workaroundProtocolConformanceReverseIteration();
26+
bool useLegacyProtocolConformanceReverseIteration();
2727

2828
/// Whether we should crash when we encounter a non-nullable Obj-C
2929
/// reference with a null value as the source of a cast.
3030
/// Default is true starting with Swift 5.4.
31-
bool unexpectedObjCNullWhileCastingIsFatal();
31+
bool useLegacyPermissiveObjCNullSemanticsInCasting();
3232

3333
/// Whether we should use the legacy semantics for casting nil optionals
3434
/// to nested optionals
35-
bool useLegacyOptionalNilInjection();
35+
bool useLegacyOptionalNilInjectionInCasting();
36+
37+
/// Whether to use legacy semantics when boxing Swift values for
38+
/// Obj-C interop
39+
bool useLegacyObjCBoxingInCasting();
3640

3741
} // namespace bincompat
3842

stdlib/public/runtime/Bincompat.cpp

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,44 @@ namespace runtime {
3333

3434
namespace bincompat {
3535

36+
#if BINARY_COMPATIBILITY_APPLE
37+
enum sdk_test {
38+
oldOS, // Can't tell the app SDK used because this is too old an OS
39+
oldApp,
40+
newApp
41+
};
42+
static enum sdk_test isAppAtLeast(dyld_build_version_t version) {
43+
if (__builtin_available(macOS 11.3, iOS 14.5, tvOS 14.5, watchOS 7.4, *)) {
44+
// Query the SDK version used to build the currently-running executable
45+
if (dyld_program_sdk_at_least(version)) {
46+
return newApp;
47+
} else {
48+
return oldApp;
49+
}
50+
}
51+
// Older Apple OS lack the ability to test the SDK version of the running app
52+
return oldOS;
53+
}
54+
55+
static enum sdk_test isAppAtLeastSpring2021() {
56+
const dyld_build_version_t spring_2021_os_versions = {0xffffffff, 0x007e50301};
57+
return isAppAtLeast(spring_2021_os_versions);
58+
}
59+
#endif
60+
3661
// Should we mimic the old override behavior when scanning protocol conformance records?
3762

3863
// Old apps expect protocol conformances to override each other in a particular
3964
// order. Starting with Swift 5.4, that order has changed as a result of
4065
// significant performance improvements to protocol conformance scanning. If
4166
// this returns `true`, the protocol conformance scan will do extra work to
4267
// mimic the old override behavior.
43-
bool workaroundProtocolConformanceReverseIteration() {
68+
bool useLegacyProtocolConformanceReverseIteration() {
4469
#if BINARY_COMPATIBILITY_APPLE
45-
// If this is a newer Apple OS ...
46-
if (__builtin_available(macOS 11.3, iOS 14.5, tvOS 14.5, watchOS 7.4, *)) {
47-
const dyld_build_version_t spring_2021_os_versions = {0xffffffff, 0x007e50301};
48-
// ... but the app was compiled before Spring 2021, use the legacy behavior.
49-
return !dyld_program_sdk_at_least(spring_2021_os_versions);
50-
} else {
51-
return false; // Use new (non-legacy) behavior on old Apple OSes
70+
switch (isAppAtLeastSpring2021()) {
71+
case oldOS: return false; // New (non-legacy) behavior on old OSes
72+
case oldApp: return true; // Legacy behavior for pre-Spring 2021 apps on new OS
73+
case newApp: return false; // New behavior for new apps
5274
}
5375
#else
5476
return false; // Never use the legacy behavior on non-Apple OSes
@@ -63,18 +85,15 @@ bool workaroundProtocolConformanceReverseIteration() {
6385
// declared non-nullable. Such null pointers can lead to undefined behavior
6486
// later on. Starting in Swift 5.4, these unexpected null pointers are fatal
6587
// runtime errors, but this is selectively disabled for old apps.
66-
bool unexpectedObjCNullWhileCastingIsFatal() {
88+
bool useLegacyPermissiveObjCNullSemanticsInCasting() {
6789
#if BINARY_COMPATIBILITY_APPLE
68-
// If this is a new enough Apple OS ...
69-
if (__builtin_available(macOS 11.3, iOS 14.5, tvOS 14.5, watchOS 7.4, *)) {
70-
const dyld_build_version_t spring_2021_os_versions = {0xffffffff, 0x007e50301};
71-
// ... use strict behavior for apps compiled on or after Spring 2021.
72-
return dyld_program_sdk_at_least(spring_2021_os_versions);
73-
} else {
74-
return false; // Use permissive behavior on old Apple OS
90+
switch (isAppAtLeastSpring2021()) {
91+
case oldOS: return true; // Permissive (legacy) behavior on old OS
92+
case oldApp: return true; // Permissive (legacy) behavior for old apps
93+
case newApp: return false; // Strict behavior for new apps
7594
}
7695
#else
77-
return true; // Always use the strict behavior on non-Apple OSes
96+
return false; // Always use the strict behavior on non-Apple OSes
7897
#endif
7998
}
8099

@@ -87,21 +106,39 @@ bool unexpectedObjCNullWhileCastingIsFatal() {
87106
// Earlier versions of the Swift runtime did not do this if the source
88107
// optional was nil. In that case, the outer target optional would be
89108
// set to nil.
90-
bool useLegacyOptionalNilInjection() {
109+
bool useLegacyOptionalNilInjectionInCasting() {
91110
#if BINARY_COMPATIBILITY_APPLE
92-
// If this is a new enough Apple OS ...
93-
if (__builtin_available(macOS 11.3, iOS 14.5, tvOS 14.5, watchOS 7.4, *)) {
94-
const dyld_build_version_t spring_2021_os_versions = {0xffffffff, 0x007e50301};
95-
// It's using Spring 2021 or later SDK, so don't use the legacy behavior.
96-
return !dyld_program_sdk_at_least(spring_2021_os_versions);
97-
} else {
98-
return true; // Use the legacy behavior on old Apple OS
111+
switch (isAppAtLeastSpring2021()) {
112+
case oldOS: return true; // Legacy behavior on old OS
113+
case oldApp: return true; // Legacy behavior for old apps
114+
case newApp: return false; // Consistent behavior for new apps
99115
}
100116
#else
101117
return false; // Always use the 5.4 behavior on non-Apple OSes
102118
#endif
103119
}
104120

121+
// Should casting be strict about protocol conformance when
122+
// boxing Swift values to pass to Obj-C?
123+
124+
// Earlier versions of the Swift runtime would allow you to
125+
// cast a swift value to e.g., `NSCopying` or `NSObjectProtocol`
126+
// even if that value did not actually conform. This was
127+
// due to the fact that the `__SwiftValue` box type itself
128+
// conformed to these protocols.
129+
130+
// But this was not really sound, as it implies for example that
131+
// `x is NSCopying` is always `true` regardless of whether
132+
// `x` actually has the `copyWithZone()` method required
133+
// by that protocol.
134+
bool useLegacyObjCBoxingInCasting() {
135+
#if BINARY_COMPATIBILITY_APPLE
136+
return true; // For now, continue using the legacy behavior on Apple OSes
137+
#else
138+
return false; // Always use the new behavior on non-Apple OSes
139+
#endif
140+
}
141+
105142
} // namespace bincompat
106143

107144
} // namespace runtime

stdlib/public/runtime/DynamicCast.cpp

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,7 @@ static HeapObject * getNonNullSrcObject(OpaqueValue *srcValue,
129129
const char * const msg = "Found unexpected null pointer value"
130130
" while trying to cast value of type '%s' (%p)"
131131
" to '%s' (%p)%s\n";
132-
if (runtime::bincompat::unexpectedObjCNullWhileCastingIsFatal()) {
133-
// By default, Swift 5.4 and later issue a fatal error.
134-
swift::fatalError(/* flags = */ 0, msg,
135-
srcTypeName.c_str(), srcType,
136-
destTypeName.c_str(), destType,
137-
"");
138-
} else {
132+
if (runtime::bincompat::useLegacyPermissiveObjCNullSemanticsInCasting()) {
139133
// In backwards compatibility mode, this code will warn and return the null
140134
// reference anyway: If you examine the calls to the function, you'll see
141135
// that most callers fail the cast in that case, but a few casts (e.g., with
@@ -145,6 +139,12 @@ static HeapObject * getNonNullSrcObject(OpaqueValue *srcValue,
145139
srcTypeName.c_str(), srcType,
146140
destTypeName.c_str(), destType,
147141
": Continuing with null object, but expect problems later.");
142+
} else {
143+
// By default, Swift 5.4 and later issue a fatal error.
144+
swift::fatalError(/* flags = */ 0, msg,
145+
srcTypeName.c_str(), srcType,
146+
destTypeName.c_str(), destType,
147+
"");
148148
}
149149
return object;
150150
}
@@ -1092,7 +1092,7 @@ tryCastUnwrappingOptionalBoth(
10921092
srcValue, /*emptyCases=*/1);
10931093
auto sourceIsNil = (sourceEnumCase != 0);
10941094
if (sourceIsNil) {
1095-
if (runtime::bincompat::useLegacyOptionalNilInjection()) {
1095+
if (runtime::bincompat::useLegacyOptionalNilInjectionInCasting()) {
10961096
auto destInnerType = cast<EnumMetadata>(destType)->getGenericArgs()[0];
10971097
// Set .none at the outer level
10981098
destInnerType->vw_storeEnumTagSinglePayload(destLocation, 1, 1);
@@ -1554,30 +1554,47 @@ tryCastToClassExistentialViaSwiftValue(
15541554
}
15551555

15561556
default: {
1557+
// We can always box when the destination is a simple
1558+
// (unconstrained) `AnyObject`.
15571559
if (destExistentialType->NumProtocols != 0) {
1558-
// The destination is a class-constrained protocol type
1559-
// and the source is not a class, so....
1560-
return DynamicCastResult::Failure;
1561-
} else {
1562-
// This is a simple (unconstrained) `AnyObject` so we can populate
1563-
// it by stuffing a non-class instance into a __SwiftValue box
1560+
// But if there are constraints...
1561+
if (!runtime::bincompat::useLegacyObjCBoxingInCasting()) {
1562+
// ... never box if we're not supporting legacy semantics.
1563+
return DynamicCastResult::Failure;
1564+
}
1565+
// Legacy behavior: We used to permit casts to a constrained (existential)
1566+
// type if the resulting `__SwiftValue` box conformed to the target type.
1567+
// This is no longer supported, since it caused `x is NSCopying` to be
1568+
// true even when x does not in fact implement the requirements of
1569+
// `NSCopying`.
15641570
#if SWIFT_OBJC_INTEROP
1565-
auto object = bridgeAnythingToSwiftValueObject(
1566-
srcValue, srcType, takeOnSuccess);
1567-
destExistentialLocation->Value = object;
1568-
if (takeOnSuccess) {
1569-
return DynamicCastResult::SuccessViaTake;
1570-
} else {
1571-
return DynamicCastResult::SuccessViaCopy;
1571+
if (!findSwiftValueConformances(
1572+
destExistentialType, destExistentialLocation->getWitnessTables())) {
1573+
return DynamicCastResult::Failure;
1574+
}
1575+
#else
1576+
if (!swift_swiftValueConformsTo(destType, destType)) {
1577+
return DynamicCastResult::Failure;
15721578
}
1573-
# else
1574-
// Note: Code below works correctly on both Obj-C and non-Obj-C platforms,
1575-
// but the code above is slightly faster on Obj-C platforms.
1576-
auto object = _bridgeAnythingToObjectiveC(srcValue, srcType);
1577-
destExistentialLocation->Value = object;
1578-
return DynamicCastResult::SuccessViaCopy;
15791579
#endif
15801580
}
1581+
1582+
#if SWIFT_OBJC_INTEROP
1583+
auto object = bridgeAnythingToSwiftValueObject(
1584+
srcValue, srcType, takeOnSuccess);
1585+
destExistentialLocation->Value = object;
1586+
if (takeOnSuccess) {
1587+
return DynamicCastResult::SuccessViaTake;
1588+
} else {
1589+
return DynamicCastResult::SuccessViaCopy;
1590+
}
1591+
# else
1592+
// Note: Code below works correctly on both Obj-C and non-Obj-C platforms,
1593+
// but the code above is slightly faster on Obj-C platforms.
1594+
auto object = _bridgeAnythingToObjectiveC(srcValue, srcType);
1595+
destExistentialLocation->Value = object;
1596+
return DynamicCastResult::SuccessViaCopy;
1597+
#endif
15811598
}
15821599
}
15831600
}

stdlib/public/runtime/ProtocolConformance.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ struct ConformanceState {
428428

429429
ConformanceState() {
430430
scanSectionsBackwards =
431-
runtime::bincompat::workaroundProtocolConformanceReverseIteration();
431+
runtime::bincompat::useLegacyProtocolConformanceReverseIteration();
432432

433433
#if USE_DYLD_SHARED_CACHE_CONFORMANCE_TABLES
434434
if (__builtin_available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)) {

test/Casting/Casts.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,15 @@ CastsTests.test("Recursive AnyHashable") {
956956

957957
// SR-14635 (aka rdar://78224322)
958958
#if _runtime(_ObjC)
959-
CastsTests.test("Do not overuse __SwiftValue") {
959+
CastsTests.test("Do not overuse __SwiftValue")
960+
.skip(.osxAny("SR-14635 not yet fully enabled for Apple OSes"))
961+
.skip(.iOSAny("SR-14635 not yet fully enabled for Apple OSes"))
962+
.skip(.iOSSimulatorAny("SR-14635 not yet fully enabled for Apple OSes"))
963+
.skip(.tvOSAny("SR-14635 not yet fully enabled for Apple OSes"))
964+
.skip(.tvOSSimulatorAny("SR-14635 not yet fully enabled for Apple OSes"))
965+
.skip(.watchOSAny("SR-14635 not yet fully enabled for Apple OSes"))
966+
.skip(.watchOSSimulatorAny("SR-14635 not yet fully enabled for Apple OSes"))
967+
.code {
960968
struct Bar {}
961969
// This used to succeed because of overeager __SwiftValue
962970
// boxing (and __SwiftValue does satisfy NSCopying)

0 commit comments

Comments
 (0)