diff --git a/runtime/sema/check_member_expression.go b/runtime/sema/check_member_expression.go index 0ff70a37f5..4d1d044a0d 100644 --- a/runtime/sema/check_member_expression.go +++ b/runtime/sema/check_member_expression.go @@ -455,6 +455,28 @@ func (checker *Checker) mapAccess( // pretend that the access succeeds to prevent a redundant access error report return true, UnauthorizedAccess } + // when we are in an assignment statement, + // we need full permissions to the map regardless of the input authorization of the reference + // Consider: + // + // entitlement X + // entitlement Y + // entitlement mapping M { + // X -> Insert + // Y -> Remove + // } + // struct S { + // access(M) var member: auth(M) &[T]? + // ... + // } + // + // If we were able to assign a `auth(Insert) &[T]` value to `ref.member` when `ref` has type `auth(X) &S` + // we could use this to then extract a `auth(Insert, Remove) &[T]` reference to that array by accessing `member` + // on an owned copy of `S`. As such, when in an assignment, we return the full codomain here as the "granted authorization" + // of the access expression, since the checker will later enforce that the incoming reference value is a subtype of that full codomain. + if checker.inAssignment { + return true, mappedAccess.Codomain() + } return true, grantedAccess case *OptionalType: diff --git a/runtime/tests/checker/entitlements_test.go b/runtime/tests/checker/entitlements_test.go index 3a997c54d5..727c0f613d 100644 --- a/runtime/tests/checker/entitlements_test.go +++ b/runtime/tests/checker/entitlements_test.go @@ -7369,3 +7369,74 @@ func TestCheckEntitlementMissingInMap(t *testing.T) { require.IsType(t, &sema.InvalidNonEntitlementTypeInMapError{}, errors[0]) }) } + +func TestInterpretMappingEscalation(t *testing.T) { + + t.Parallel() + + t.Run("escalate", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement X + entitlement Y + entitlement mapping M { + X -> Insert + Y -> Remove + } + struct S { + access(M) var member: auth(M) &[Int]? + init() { + self.member = nil; + } + access(all) fun grantRemovePrivileges(param: auth(Insert) &[Int]): Void{ + var selfRef = &self as auth(X) &S; + selfRef.member = param; + } + } + fun main(): Void { + var arr: [Int] = [123]; + var arrRef = &arr as auth(Insert) &[Int]; + let s = S() + s.grantRemovePrivileges(param: arrRef); + s.member?.removeLast() + } + `) + + errors := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.TypeMismatchError{}, errors[0]) + }) + + t.Run("field assign", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement X + entitlement Y + entitlement mapping M { + X -> Insert + Y -> Remove + } + struct S { + access(M) var member: auth(M) &[Int]? + init() { + self.member = nil; + } + access(all) fun grantRemovePrivileges(sRef: auth(X) &S, param: auth(Insert) &[Int]): Void{ + sRef.member = param; + } + } + fun main(): Void { + var arr: [Int] = [123]; + var arrRef = &arr as auth(Insert) &[Int]; + let s = S() + s.grantRemovePrivileges(sRef: &s as auth(X) &S, param: arrRef); + s.member?.removeLast() + } + `) + + errors := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.TypeMismatchError{}, errors[0]) + }) + +}