Serialization: restrict swiftmodules to distribution channels

There are scenarios where different compilers are distributed with
compatible serialization format versions and the same tag. Distinguish
swiftmodules in such a case by assigning them to different distribution
channels. A compiler expecting a specific channel will only read
swiftmodules from the same channel. The channels should be defined by
downstream code as it is by definition vendor specific.

For development, a no-channel compiler loads or defining the env var
SWIFT_IGNORE_SWIFTMODULE_REVISION skips this new check.

rdar://123731777
This commit is contained in:
Alexis Laferrière
2024-02-29 17:48:34 -08:00
parent ffc2d9f9a7
commit 1e4fe67f40
10 changed files with 149 additions and 1 deletions

View File

@@ -850,6 +850,11 @@ ERROR(serialization_module_incompatible_revision,Fatal,
"compiled module was created by a different version of the compiler '%0'; "
"rebuild %1 and try again: %2",
(StringRef, Identifier, StringRef))
ERROR(serialization_module_incompatible_channel,Fatal,
"compiled module was created for a different distribution channel '%0' "
"than the local compiler '%1', "
"please ensure %2 is found from the expected path: %3",
(StringRef, StringRef, Identifier, StringRef))
ERROR(serialization_missing_single_dependency,Fatal,
"missing required module '%0'", (StringRef))
ERROR(serialization_missing_dependencies,Fatal,

View File

@@ -177,6 +177,11 @@ StringRef getCurrentCompilerTag();
/// depending on the vendor.
StringRef getCurrentCompilerSerializationTag();
/// Distribution channel of the running compiler for distributed swiftmodules.
/// Helps to distinguish swiftmodules between different compilers using the
/// same serialization tag.
StringRef getCurrentCompilerChannel();
/// Retrieves the value of the upcoming C++ interoperability compatibility
/// version that's going to be presented as some new concrete version to the
/// users.

View File

@@ -48,6 +48,9 @@ enum class Status {
/// The precise revision version doesn't match.
RevisionIncompatible,
/// The distribution channel doesn't match.
ChannelIncompatible,
/// The module is required to be in OSSA, but is not.
NotInOSSA,
@@ -105,6 +108,7 @@ struct ValidationInfo {
llvm::VersionTuple userModuleVersion;
StringRef sdkName = {};
StringRef problematicRevision = {};
StringRef problematicChannel = {};
size_t bytes = 0;
Status status = Status::Malformed;
std::vector<StringRef> allowableClients;

View File

@@ -313,6 +313,16 @@ StringRef getCurrentCompilerSerializationTag() {
#endif
}
StringRef getCurrentCompilerChannel() {
static const char* forceDebugChannel =
::getenv("SWIFT_FORCE_SWIFTMODULE_CHANNEL");
if (forceDebugChannel)
return forceDebugChannel;
// Leave it to downstream compilers to define the different channels.
return StringRef();
}
unsigned getUpcomingCxxInteropCompatVersion() {
return SWIFT_VERSION_MAJOR + 1;
}

View File

@@ -316,6 +316,8 @@ struct ModuleRebuildInfo {
return "compiled with a newer version of the compiler";
case Status::RevisionIncompatible:
return "compiled with a different version of the compiler";
case Status::ChannelIncompatible:
return "compiled for a different distribution channel";
case Status::NotInOSSA:
return "module was not built with OSSA";
case Status::NoncopyableGenericsMismatch:

View File

@@ -418,6 +418,21 @@ static ValidationInfo validateControlBlock(
}
break;
}
case control_block::CHANNEL: {
static const char* ignoreRevision =
::getenv("SWIFT_IGNORE_SWIFTMODULE_REVISION");
if (ignoreRevision)
break;
StringRef moduleChannel = blobData,
compilerChannel = version::getCurrentCompilerChannel();
if (requiresRevisionMatch && !compilerChannel.empty() &&
moduleChannel != compilerChannel) {
result.problematicChannel = moduleChannel;
result.status = Status::ChannelIncompatible;
}
break;
}
case control_block::IS_OSSA: {
auto isModuleInOSSA = scratch[0];
if (requiresOSSAModules && !isModuleInOSSA)
@@ -532,6 +547,7 @@ std::string serialization::StatusToString(Status S) {
case Status::FormatTooOld: return "FormatTooOld";
case Status::FormatTooNew: return "FormatTooNew";
case Status::RevisionIncompatible: return "RevisionIncompatible";
case Status::ChannelIncompatible: return "ChannelIncompatible";
case Status::NotInOSSA: return "NotInOSSA";
case Status::NoncopyableGenericsMismatch:
return "NoncopyableGenericsMismatch";

View File

@@ -58,7 +58,7 @@ const uint16_t SWIFTMODULE_VERSION_MAJOR = 0;
/// describe what change you made. The content of this comment isn't important;
/// it just ensures a conflict if two people change the module format.
/// Don't worry about adhering to the 80-column limit for this line.
const uint16_t SWIFTMODULE_VERSION_MINOR = 858; // Replace inherited types with protocols in protocol layout
const uint16_t SWIFTMODULE_VERSION_MINOR = 859; // channel check
/// A standard hash seed used for all string hashes in a serialized module.
///
@@ -860,6 +860,7 @@ namespace control_block {
TARGET,
SDK_NAME,
REVISION,
CHANNEL,
IS_OSSA,
ALLOWABLE_CLIENT_NAME,
HAS_NONCOPYABLE_GENERICS,
@@ -898,6 +899,11 @@ namespace control_block {
BCBlob
>;
using ChannelLayout = BCRecordLayout<
CHANNEL,
BCBlob
>;
using IsOSSALayout = BCRecordLayout<
IS_OSSA,
BCFixed<1>

View File

@@ -834,6 +834,7 @@ void Serializer::writeBlockInfoBlock() {
BLOCK_RECORD(control_block, TARGET);
BLOCK_RECORD(control_block, SDK_NAME);
BLOCK_RECORD(control_block, REVISION);
BLOCK_RECORD(control_block, CHANNEL);
BLOCK_RECORD(control_block, IS_OSSA);
BLOCK_RECORD(control_block, ALLOWABLE_CLIENT_NAME);
BLOCK_RECORD(control_block, HAS_NONCOPYABLE_GENERICS);
@@ -982,6 +983,7 @@ void Serializer::writeHeader() {
control_block::TargetLayout Target(Out);
control_block::SDKNameLayout SDKName(Out);
control_block::RevisionLayout Revision(Out);
control_block::ChannelLayout Channel(Out);
control_block::IsOSSALayout IsOSSA(Out);
control_block::AllowableClientLayout Allowable(Out);
control_block::HasNoncopyableGenerics HasNoncopyableGenerics(Out);
@@ -1035,6 +1037,8 @@ void Serializer::writeHeader() {
forcedDebugRevision : version::getCurrentCompilerSerializationTag();
Revision.emit(ScratchRecord, revision);
Channel.emit(ScratchRecord, version::getCurrentCompilerChannel());
IsOSSA.emit(ScratchRecord, Options.IsOSSA);
HasNoncopyableGenerics.emit(ScratchRecord,

View File

@@ -1081,6 +1081,12 @@ void swift::serialization::diagnoseSerializedASTLoadFailure(
Ctx.Diags.diagnose(diagLoc, diag::serialization_module_incompatible_revision,
loadInfo.problematicRevision, ModuleName, moduleBufferID);
break;
case serialization::Status::ChannelIncompatible:
Ctx.Diags.diagnose(diagLoc, diag::serialization_module_incompatible_channel,
loadInfo.problematicChannel,
version::getCurrentCompilerChannel(),
ModuleName, moduleBufferID);
break;
case serialization::Status::Malformed:
Ctx.Diags.diagnose(diagLoc, diag::serialization_malformed_module,
moduleBufferID);
@@ -1168,6 +1174,7 @@ void swift::serialization::diagnoseSerializedASTLoadFailureTransitive(
case serialization::Status::NotInOSSA:
case serialization::Status::NoncopyableGenericsMismatch:
case serialization::Status::RevisionIncompatible:
case serialization::Status::ChannelIncompatible:
case serialization::Status::Malformed:
case serialization::Status::MalformedDocumentation:
case serialization::Status::FailedToLoadBridgingHeader:

View File

@@ -0,0 +1,89 @@
// Binary swiftmodules are restricted to a channel when set in the compiler.
// RUN: %empty-directory(%t/cache)
// RUN: %empty-directory(%t/build)
// RUN: split-file %s %t --leading-lines
//--- Lib.swift
public func foo() {}
/// Build Lib as a resilient and non-resilient swiftmodule
// RUN: %target-swift-frontend -emit-module %t/Lib.swift -swift-version 5 \
// RUN: -o %t/build -parse-stdlib -module-cache-path %t/cache \
// RUN: -module-name ResilientLib -enable-library-evolution \
// RUN: -emit-module-interface-path %t/build/ResilientLib.swiftinterface
// RUN: %target-swift-frontend -emit-module %t/Lib.swift -swift-version 5 \
// RUN: -o %t/build -parse-stdlib -module-cache-path %t/cache \
// RUN: -module-name NonResilientLib
/// Build a channel restricted Lib.
// RUN: env SWIFT_FORCE_SWIFTMODULE_CHANNEL=restricted-channel \
// RUN: %target-swift-frontend -emit-module %t/Lib.swift -swift-version 5 \
// RUN: -o %t/build -parse-stdlib -module-cache-path %t/cache \
// RUN: -module-name ChannelLib -enable-library-evolution
/// 2. Test importing the non-resilient no-channel library from a channel compiler.
//--- NonResilientClient.swift
import NonResilientLib // expected-error {{compiled module was created for a different distribution channel '' than the local compiler 'restricted-channel', please ensure 'NonResilientLib' is found from the expected path:}}
foo()
/// Building a NonResilientLib client should reject the import for a tagged compiler
// RUN: env SWIFT_FORCE_SWIFTMODULE_CHANNEL=restricted-channel \
// RUN: %target-swift-frontend -typecheck %t/NonResilientClient.swift \
// RUN: -swift-version 5 -I %t/build -parse-stdlib -module-cache-path %t/cache \
// RUN: -verify -verify-ignore-unknown
/// 3. Test importing the resilient no-channel library.
//--- ResilientClient.swift
import ResilientLib // expected-reject-error {{compiled module was created for a different distribution channel '' than the local compiler 'restricted-channel', please ensure 'ResilientLib' is found from the expected path:}}
// expected-rebuild-remark @-1 {{rebuilding module 'ResilientLib' from interface}}
// expected-rebuild-note @-2 {{compiled module is out of date}}
// expected-rebuild-note @-3 {{compiled for a different distribution channel}}
foo()
/// ResilientLib client should rebuild from swiftinterface when available.
// RUN: env SWIFT_FORCE_SWIFTMODULE_CHANNEL=restricted-channel \
// RUN: %target-swift-frontend -typecheck %t/ResilientClient.swift \
// RUN: -swift-version 5 -I %t/build -parse-stdlib -module-cache-path %t/cache \
// RUN: -verify -verify-additional-prefix rebuild- -Rmodule-interface-rebuild
// RUN: rm %t/build/ResilientLib.swiftinterface
// RUN: %empty-directory(%t/cache)
/// Building a ResilientLib client should succeed in no-channel / dev mode.
// RUN: %target-swift-frontend -typecheck %t/ResilientClient.swift \
// RUN: -swift-version 5 -I %t/build -parse-stdlib -module-cache-path %t/cache
/// Building a ResilientLib client should reject the import for a channel compiler.
// RUN: env SWIFT_FORCE_SWIFTMODULE_CHANNEL=restricted-channel \
// RUN: %target-swift-frontend -typecheck %t/ResilientClient.swift \
// RUN: -swift-version 5 -I %t/build -parse-stdlib -module-cache-path %t/cache \
// RUN: -verify -verify-ignore-unknown -verify-additional-prefix reject-
/// Building a ResilientLib client should succeed for a channel compiler with SWIFT_IGNORE_SWIFTMODULE_REVISION
// RUN: env SWIFT_FORCE_SWIFTMODULE_CHANNEL=restricted-channel SWIFT_IGNORE_SWIFTMODULE_REVISION=true \
// RUN: %target-swift-frontend -typecheck %t/ResilientClient.swift \
// RUN: -swift-version 5 -I %t/build -parse-stdlib -module-cache-path %t/cache
/// 4. Test importing the channel restricted library
//--- ChannelClient.swift
import ChannelLib // expected-reject-error {{compiled module was created for a different distribution channel 'restricted-channel' than the local compiler 'other-channel', please ensure 'ChannelLib' is found from the expected path}}
foo()
/// Importing ChannelLib should succeed with the same channel.
// RUN: env SWIFT_FORCE_SWIFTMODULE_CHANNEL=restricted-channel \
// RUN: %target-swift-frontend -typecheck %t/ChannelClient.swift \
// RUN: -swift-version 5 -I %t/build -parse-stdlib -module-cache-path %t/cache
/// Importing ChannelLib should succeed with a dev compiler.
// RUN: %target-swift-frontend -typecheck %t/ChannelClient.swift \
// RUN: -swift-version 5 -I %t/build -parse-stdlib -module-cache-path %t/cache
/// Importing ChannelLib from a different channel should be rejected.
// RUN: env SWIFT_FORCE_SWIFTMODULE_CHANNEL=other-channel \
// RUN: %target-swift-frontend -typecheck %t/ChannelClient.swift \
// RUN: -swift-version 5 -I %t/build -parse-stdlib -module-cache-path %t/cache \
// RUN: -verify -verify-ignore-unknown -verify-additional-prefix reject-