Skip to content

Commit a5579d1

Browse files
committed
AST: Plug a hole in access control checking
A protocol extension of a private protocol can define internal or public members. We should not be able to find these members from another file or module if an internal or public type conforms to the protocol. Fixes <rdar://problem/21380336>.
1 parent 431dd1c commit a5579d1

15 files changed

+304
-13
lines changed

include/swift/AST/Decl.h

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2265,6 +2265,18 @@ class ValueDecl : public Decl {
22652265
return result;
22662266
}
22672267

2268+
/// If this declaration is a member of a protocol extension, return the
2269+
/// minimum of the given access level and the protocol's access level.
2270+
///
2271+
/// Otherwise, return the given access level unmodified.
2272+
///
2273+
/// This is used when checking name lookup visibility. Protocol extension
2274+
/// members can be found when performing name lookup on a concrete type;
2275+
/// if the concrete type is visible from the lookup context but the
2276+
/// protocol is not, we do not want the protocol extension members to be
2277+
/// visible.
2278+
AccessLevel adjustAccessLevelForProtocolExtension(AccessLevel access) const;
2279+
22682280
/// Determine whether this Decl has either Private or FilePrivate access.
22692281
bool isOutermostPrivateOrFilePrivateScope() const;
22702282

@@ -2325,7 +2337,14 @@ class ValueDecl : public Decl {
23252337
/// A public declaration is accessible everywhere.
23262338
///
23272339
/// If \p DC is null, returns true only if this declaration is public.
2328-
bool isAccessibleFrom(const DeclContext *DC) const;
2340+
///
2341+
/// If \p forConformance is true, we ignore the visibility of the protocol
2342+
/// when evaluating protocol extension members. This language rule allows a
2343+
/// protocol extension of a private protocol to provide default
2344+
/// implementations for the requirements of a public protocol, even when
2345+
/// the default implementations are not visible to name lookup.
2346+
bool isAccessibleFrom(const DeclContext *DC,
2347+
bool forConformance = false) const;
23292348

23302349
/// Retrieve the "interface" type of this value, which uses
23312350
/// GenericTypeParamType if the declaration is generic. For a generic
@@ -4449,7 +4468,11 @@ class AbstractStorageDecl : public ValueDecl {
44494468
/// context.
44504469
///
44514470
/// If \p DC is null, returns true only if the setter is public.
4452-
bool isSetterAccessibleFrom(const DeclContext *DC) const;
4471+
///
4472+
/// See \c isAccessibleFrom for a discussion of the \p forConformance
4473+
/// parameter.
4474+
bool isSetterAccessibleFrom(const DeclContext *DC,
4475+
bool forConformance=false) const;
44534476

44544477
/// Determine how this storage declaration should actually be accessed.
44554478
AccessStrategy getAccessStrategy(AccessSemantics semantics,

lib/AST/NameLookup.cpp

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,12 +1507,36 @@ void ClassDecl::recordObjCMethod(AbstractFunctionDecl *method) {
15071507
vec.push_back(method);
15081508
}
15091509

1510-
static bool checkAccess(const DeclContext *useDC, const DeclContext *sourceDC,
1511-
AccessLevel access) {
1510+
AccessLevel
1511+
ValueDecl::adjustAccessLevelForProtocolExtension(AccessLevel access) const {
1512+
if (auto *ext = dyn_cast<ExtensionDecl>(getDeclContext())) {
1513+
if (auto *protocol = ext->getAsProtocolOrProtocolExtensionContext()) {
1514+
// Note: it gets worse. The standard library has public methods
1515+
// in protocol extensions of a @usableFromInline internal protocol,
1516+
// and expects these extension methods to witness public protocol
1517+
// requirements. Which works at the ABI level, so let's keep
1518+
// supporting that here by passing 'isUsageFromInline'.
1519+
auto protoAccess = protocol->getFormalAccess(/*useDC=*/nullptr,
1520+
/*isUsageFromInline=*/true);
1521+
if (protoAccess == AccessLevel::Private)
1522+
protoAccess = AccessLevel::FilePrivate;
1523+
access = std::min(access, protoAccess);
1524+
}
1525+
}
1526+
1527+
return access;
1528+
}
1529+
1530+
static bool checkAccess(const DeclContext *useDC, const ValueDecl *VD,
1531+
AccessLevel access, bool forConformance) {
15121532
if (!useDC)
15131533
return access >= AccessLevel::Public;
15141534

1515-
assert(sourceDC && "ValueDecl being accessed must have a valid DeclContext");
1535+
auto *sourceDC = VD->getDeclContext();
1536+
1537+
if (!forConformance)
1538+
access = VD->adjustAccessLevelForProtocolExtension(access);
1539+
15161540
switch (access) {
15171541
case AccessLevel::Private:
15181542
return (useDC == sourceDC ||
@@ -1536,11 +1560,14 @@ static bool checkAccess(const DeclContext *useDC, const DeclContext *sourceDC,
15361560
llvm_unreachable("bad access level");
15371561
}
15381562

1539-
bool ValueDecl::isAccessibleFrom(const DeclContext *DC) const {
1540-
return checkAccess(DC, getDeclContext(), getFormalAccess());
1563+
bool ValueDecl::isAccessibleFrom(const DeclContext *DC,
1564+
bool forConformance) const {
1565+
auto access = getFormalAccess();
1566+
return checkAccess(DC, this, access, forConformance);
15411567
}
15421568

1543-
bool AbstractStorageDecl::isSetterAccessibleFrom(const DeclContext *DC) const {
1569+
bool AbstractStorageDecl::isSetterAccessibleFrom(const DeclContext *DC,
1570+
bool forConformance) const {
15441571
assert(isSettable(DC));
15451572

15461573
// If a stored property does not have a setter, it is still settable from the
@@ -1552,7 +1579,8 @@ bool AbstractStorageDecl::isSetterAccessibleFrom(const DeclContext *DC) const {
15521579
if (isa<ParamDecl>(this))
15531580
return true;
15541581

1555-
return checkAccess(DC, getDeclContext(), getSetterFormalAccess());
1582+
auto access = getSetterFormalAccess();
1583+
return checkAccess(DC, this, access, forConformance);
15561584
}
15571585

15581586
Type AbstractStorageDecl::getStorageInterfaceType() const {

lib/Sema/CSDiag.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1546,8 +1546,15 @@ diagnoseUnviableLookupResults(MemberLookupResult &result, Type baseObjTy,
15461546
case MemberLookupResult::UR_Inaccessible: {
15471547
auto decl = result.UnviableCandidates[0].first.getDecl();
15481548
// FIXME: What if the unviable candidates have different levels of access?
1549+
//
1550+
// If we found an inaccessible member of a protocol extension, it might
1551+
// be declared 'public'. This can only happen if the protocol is not
1552+
// visible to us, but the conforming type is. In this case, we need to
1553+
// clamp the formal access for diagnostics purposes to the formal access
1554+
// of the protocol itself.
15491555
diagnose(nameLoc, diag::candidate_inaccessible, decl->getBaseName(),
1550-
decl->getFormalAccess());
1556+
decl->adjustAccessLevelForProtocolExtension(
1557+
decl->getFormalAccess()));
15511558
for (auto cand : result.UnviableCandidates)
15521559
diagnose(cand.first.getDecl(), diag::decl_declared_here, memberName);
15531560

lib/Sema/TypeCheckNameLookup.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ namespace {
5454
NameLookupOptions options,
5555
bool isMemberLookup)
5656
: TC(tc), Result(result), DC(dc), Options(options),
57-
IsMemberLookup(isMemberLookup) { }
57+
IsMemberLookup(isMemberLookup) {
58+
if (!TC.Context.LangOpts.EnableAccessControl)
59+
Options |= NameLookupFlags::IgnoreAccessControl;
60+
}
5861

5962
~LookupResultBuilder() {
6063
// If any of the results have a base, we need to remove
@@ -184,6 +187,16 @@ namespace {
184187
.second;
185188
} else if (found->isProtocolRequirement()) {
186189
witness = concrete->getWitnessDecl(found, &TC);
190+
191+
// It is possible that a requirement is visible to us, but
192+
// not the witness. In this case, just return the requirement;
193+
// we will perform virtual dispatch on the concrete type.
194+
if (witness &&
195+
!Options.contains(NameLookupFlags::IgnoreAccessControl) &&
196+
!witness->isAccessibleFrom(DC)) {
197+
addResult(found);
198+
return;
199+
}
187200
}
188201

189202
// FIXME: the "isa<ProtocolDecl>()" check will be wrong for

lib/Sema/TypeCheckProtocol.cpp

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,14 +1051,21 @@ bool WitnessChecker::checkWitnessAccess(AccessScope &requiredAccessScope,
10511051
bool *isSetter) {
10521052
*isSetter = false;
10531053

1054+
// Compute the intersection of the conforming type's access scope
1055+
// and the protocol's access scope.
10541056
auto scopeIntersection =
10551057
requiredAccessScope.intersectWith(Proto->getFormalAccessScope(DC));
10561058
assert(scopeIntersection.hasValue());
10571059

10581060
requiredAccessScope = *scopeIntersection;
10591061

10601062
AccessScope actualScopeToCheck = requiredAccessScope;
1061-
if (!witness->isAccessibleFrom(actualScopeToCheck.getDeclContext())) {
1063+
1064+
// Setting the 'forConformance' flag means that we admit witnesses in
1065+
// protocol extensions that we can see, but are not necessarily as
1066+
// visible as the conforming type and protocol.
1067+
if (!witness->isAccessibleFrom(actualScopeToCheck.getDeclContext(),
1068+
/*forConformance=*/true)) {
10621069
// Special case: if we have `@testable import` of the witness's module,
10631070
// allow the witness to match if it would have matched for just this file.
10641071
// That is, if '@testable' allows us to see the witness here, it should
@@ -1081,7 +1088,10 @@ bool WitnessChecker::checkWitnessAccess(AccessScope &requiredAccessScope,
10811088
*isSetter = true;
10821089

10831090
auto ASD = cast<AbstractStorageDecl>(witness);
1084-
if (!ASD->isSetterAccessibleFrom(actualScopeToCheck.getDeclContext()))
1091+
1092+
// See above about the forConformance flag.
1093+
if (!ASD->isSetterAccessibleFrom(actualScopeToCheck.getDeclContext(),
1094+
/*forConformance=*/true))
10851095
return true;
10861096
}
10871097

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
public protocol P {
2+
func publicRequirement()
3+
}
4+
5+
@usableFromInline
6+
protocol Q : P {
7+
func internalRequirement()
8+
}
9+
10+
fileprivate protocol R : Q {}
11+
12+
extension R {
13+
public func publicRequirement() {}
14+
}
15+
16+
extension Q {
17+
public func internalRequirement() {}
18+
}
19+
20+
public struct S : R {}

test/SILGen/testable-multifile-other.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// RUN: %target-swift-frontend -emit-ir -enable-sil-ownership -I %t -primary-file %s %S/testable-multifile.swift -module-name main -o /dev/null
1313
// RUN: %target-swift-frontend -emit-ir -enable-sil-ownership -I %t -O -primary-file %s %S/testable-multifile.swift -module-name main -o /dev/null
1414

15+
@testable import TestableMultifileHelper
16+
1517
func use<F: Fooable>(_ f: F) { f.foo() }
1618
func test(internalFoo: FooImpl, publicFoo: PublicFooImpl) {
1719
use(internalFoo)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-frontend -emit-module-path %t/witness_accessibility_other.swiftmodule %S/Inputs/witness_accessibility_other.swift
3+
// RUN: %target-swift-frontend -I %t -emit-silgen -enable-sil-ownership %s | %FileCheck %s
4+
// RUN: %target-swift-frontend -I %t -emit-sil -enable-sil-ownership %s
5+
6+
import witness_accessibility_other
7+
8+
// We can see the conformance S : P, but we cannot see the witness for
9+
// P.publicRequirement() because it is defined in an extension of a
10+
// private protocol R to which S also conforms.
11+
//
12+
// So make sure the witness is invoked via virtual dispatch even with
13+
// a concrete base type.
14+
15+
// CHECK-LABEL: sil @$S27witness_accessibility_multi22callsPublicRequirement1sy0a1_B6_other1SV_tF : $@convention(thin) (S) -> ()
16+
public func callsPublicRequirement(s: S) {
17+
18+
// CHECK: witness_method $S, #P.publicRequirement!1 : <Self where Self : P> (Self) -> () -> () : $@convention(witness_method: P) <τ_0_0 where τ_0_0 : P> (@in_guaranteed τ_0_0) -> ()
19+
s.publicRequirement()
20+
21+
// CHECK: function_ref @$S27witness_accessibility_other1QPAAE19internalRequirementyyF : $@convention(method) <τ_0_0 where τ_0_0 : Q> (@in_guaranteed τ_0_0) -> ()
22+
s.internalRequirement()
23+
24+
// CHECK: return
25+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
public protocol PublicProtocol {
2+
func publicRequirement()
3+
}
4+
5+
private protocol PrivateProtocol : PublicProtocol {}
6+
7+
public struct Conformer : PrivateProtocol {}
8+
9+
extension PrivateProtocol {
10+
// This implementation is opaque to callers, since we can
11+
// only find it via a private protocol.
12+
public func publicRequirement() {}
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-frontend -emit-module -emit-module-path %t/opaque_conformance.swiftmodule -primary-file %S/Inputs/opaque_conformance.swift
3+
// RUN: %target-swift-frontend -O -emit-sil -primary-file %s -I %t | %FileCheck %s
4+
5+
import opaque_conformance
6+
7+
public func callsPublicRequirement(_ c: Conformer) {
8+
c.publicRequirement()
9+
}
10+
11+
// CHECK-LABEL: sil @$S21devirt_opaque_witness22callsPublicRequirementyy0B12_conformance9ConformerVF : $@convention(thin) (Conformer) -> () {
12+
// CHECK: bb0(%0 : $Conformer):
13+
// CHECK: [[BOX:%.*]] = alloc_stack $Conformer
14+
// CHECK-NEXT: store %0 to [[BOX]] : $*Conformer
15+
// CHECK: [[FN:%.*]] = function_ref @$S18opaque_conformance9ConformerVAA14PublicProtocolA2aDP17publicRequirementyyFTW : $@convention(witness_method: PublicProtocol) (@in_guaranteed Conformer) -> ()
16+
// CHECK-NEXT: [[RESULT:%.*]] = apply [[FN]]([[BOX]]) : $@convention(witness_method: PublicProtocol) (@in_guaranteed Conformer) -> ()
17+
// CHECK-NEXT: dealloc_stack [[BOX]] : $*Conformer
18+
// CHECK-NEXT: [[RESULT:%.*]] = tuple ()
19+
// CHECK-NEXT: return [[RESULT]] : $()
20+
21+
// Note that [transparent] here doesn't mean anything since there's no body.
22+
// The important thing is that the witness_method call got devirtualized,
23+
// and the thunk has public linkage and no serialized body (since the body
24+
// references a private symbol of the module opaque_conformance).
25+
// CHECK-LABEL: sil [transparent] [thunk] @$S18opaque_conformance9ConformerVAA14PublicProtocolA2aDP17publicRequirementyyFTW : $@convention(witness_method: PublicProtocol) (@in_guaranteed Conformer) -> ()
26+
27+
// CHECK-LABEL: sil_witness_table public_external Conformer: PublicProtocol module opaque_conformance {
28+
// CHECK-NEXT: method #PublicProtocol.publicRequirement!1: <Self where Self : PublicProtocol> (Self) -> () -> () : @$S18opaque_conformance9ConformerVAA14PublicProtocolA2aDP17publicRequirementyyFTW
29+
// CHECK-NEXT: }

test/Sema/Inputs/accessibility_multi_other.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,15 @@ struct Members {
77
set {}
88
}
99
}
10+
11+
struct PrivateConformance : PrivateProtocol {}
12+
13+
private protocol PrivateProtocol {}
14+
15+
extension PrivateProtocol {
16+
public func publicExtensionMember() {}
17+
// expected-note@-1 {{'publicExtensionMember' declared here}}
18+
19+
internal func internalExtensionMember() {}
20+
// expected-note@-1 {{'internalExtensionMember' declared here}}
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
public struct PrivateConformance : PrivateProtocol {}
2+
3+
private protocol PrivateProtocol {}
4+
5+
extension PrivateProtocol {
6+
public func publicExtensionMember() {}
7+
8+
internal func internalExtensionMember() {}
9+
}
10+
11+
public struct InternalConformance : InternalProtocol {}
12+
13+
internal protocol InternalProtocol {}
14+
15+
extension InternalProtocol {
16+
public func publicExtensionMember() {}
17+
18+
internal func internalExtensionMember() {}
19+
}

test/Sema/accessibility_multi.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ func testSubscript(_ instance: Members) {
2121
instance[] = 42 // expected-error {{cannot assign through subscript: subscript setter is inaccessible}}
2222
reset(&instance[]) // expected-error {{cannot pass immutable value as inout argument: subscript setter is inaccessible}}
2323
}
24+
25+
func testPrivateConformance(_ instance: PrivateConformance) {
26+
instance.publicExtensionMember()
27+
// expected-error@-1 {{'publicExtensionMember' is inaccessible due to 'fileprivate' protection level}}
28+
29+
instance.internalExtensionMember()
30+
// expected-error@-1 {{'internalExtensionMember' is inaccessible due to 'fileprivate' protection level}}
31+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-frontend -emit-module -primary-file %S/Inputs/accessibility_multi_other_module.swift -emit-module-path %t/accessibility_multi_other_module.swiftmodule
3+
// RUN: %target-swift-frontend -typecheck -I %t -primary-file %s -verify -verify-ignore-unknown
4+
5+
import accessibility_multi_other_module
6+
7+
func testPrivateConformance(_ instance: PrivateConformance) {
8+
instance.publicExtensionMember()
9+
// expected-error@-1 {{'publicExtensionMember' is inaccessible due to 'fileprivate' protection level}}
10+
11+
instance.internalExtensionMember()
12+
// expected-error@-1 {{'internalExtensionMember' is inaccessible due to 'fileprivate' protection level}}
13+
}
14+
15+
func testInternalConformance(_ instance: InternalConformance) {
16+
instance.publicExtensionMember()
17+
// expected-error@-1 {{'publicExtensionMember' is inaccessible due to 'internal' protection level}}
18+
19+
instance.internalExtensionMember()
20+
// expected-error@-1 {{'internalExtensionMember' is inaccessible due to 'internal' protection level}}
21+
}

0 commit comments

Comments
 (0)