diff --git a/runtime/interpreter/errors.go b/runtime/interpreter/errors.go index 123b553402..e7b6a46ec2 100644 --- a/runtime/interpreter/errors.go +++ b/runtime/interpreter/errors.go @@ -671,6 +671,23 @@ func (e ValueTransferTypeError) Error() string { ) } +// UnexpectedMappedEntitlementError +type UnexpectedMappedEntitlementError struct { + Type sema.Type + LocationRange +} + +var _ errors.InternalError = UnexpectedMappedEntitlementError{} + +func (UnexpectedMappedEntitlementError) IsInternalError() {} + +func (e UnexpectedMappedEntitlementError) Error() string { + return fmt.Sprintf( + "invalid transfer of value: found an unexpected runtime mapped entitlement `%s`", + e.Type.QualifiedString(), + ) +} + // ResourceConstructionError type ResourceConstructionError struct { CompositeType *sema.CompositeType diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index e35845802d..66233c2369 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -2115,6 +2115,14 @@ func (interpreter *Interpreter) convert(value Value, valueType, targetType sema. // transferring a reference at runtime does not change its entitlements; this is so that an upcast reference // can later be downcast back to its original entitlement set + // check defensively that we never create a runtime mapped entitlement value + if _, isMappedAuth := unwrappedTargetType.Authorization.(*sema.EntitlementMapAccess); isMappedAuth { + panic(UnexpectedMappedEntitlementError{ + Type: unwrappedTargetType, + LocationRange: locationRange, + }) + } + switch ref := value.(type) { case *EphemeralReferenceValue: return NewEphemeralReferenceValue( diff --git a/runtime/sema/check_function.go b/runtime/sema/check_function.go index db4204b7fb..5d706b943e 100644 --- a/runtime/sema/check_function.go +++ b/runtime/sema/check_function.go @@ -202,19 +202,23 @@ func (checker *Checker) checkFunction( functionActivation.InitializationInfo = initializationInfo if functionBlock != nil { - if mappedAccess, isMappedAccess := access.(*EntitlementMapAccess); isMappedAccess { - checker.entitlementMappingInScope = mappedAccess.Type - } - - checker.InNewPurityScope(functionType.Purity == FunctionPurityView, func() { - checker.visitFunctionBlock( - functionBlock, - functionType.ReturnTypeAnnotation, - checkResourceLoss, - ) - }) - - checker.entitlementMappingInScope = nil + func() { + oldMappedAccess := checker.entitlementMappingInScope + if mappedAccess, isMappedAccess := access.(*EntitlementMapAccess); isMappedAccess { + checker.entitlementMappingInScope = mappedAccess.Type + } else { + checker.entitlementMappingInScope = nil + } + defer func() { checker.entitlementMappingInScope = oldMappedAccess }() + + checker.InNewPurityScope(functionType.Purity == FunctionPurityView, func() { + checker.visitFunctionBlock( + functionBlock, + functionType.ReturnTypeAnnotation, + checkResourceLoss, + ) + }) + }() if mustExit { returnType := functionType.ReturnTypeAnnotation.Type diff --git a/runtime/sema/check_member_expression.go b/runtime/sema/check_member_expression.go index e673a579a1..c54159085d 100644 --- a/runtime/sema/check_member_expression.go +++ b/runtime/sema/check_member_expression.go @@ -321,12 +321,6 @@ func (checker *Checker) visitMember(expression *ast.MemberExpression, isAssignme switch ty := resultingType.(type) { case *ReferenceType: return NewReferenceType(checker.memoryGauge, resultingAuthorization, ty.Type) - case *OptionalType: - switch innerTy := ty.Type.(type) { - case *ReferenceType: - return NewOptionalType(checker.memoryGauge, - NewReferenceType(checker.memoryGauge, resultingAuthorization, innerTy.Type)) - } } return resultingType } @@ -334,16 +328,7 @@ func (checker *Checker) visitMember(expression *ast.MemberExpression, isAssignme shouldSubstituteAuthorization := !member.Access.Equal(resultingAuthorization) if shouldSubstituteAuthorization { - switch ty := resultingType.(type) { - case *FunctionType: - resultingType = NewSimpleFunctionType( - ty.Purity, - ty.Parameters, - NewTypeAnnotation(substituteConcreteAuthorization(ty.ReturnTypeAnnotation.Type)), - ) - default: - resultingType = substituteConcreteAuthorization(resultingType) - } + resultingType = resultingType.Map(checker.memoryGauge, make(map[*TypeParameter]*TypeParameter), substituteConcreteAuthorization) } // Check that the member access is not to a function of resource type diff --git a/runtime/sema/checker.go b/runtime/sema/checker.go index d206d15469..3c1656d14f 100644 --- a/runtime/sema/checker.go +++ b/runtime/sema/checker.go @@ -1256,19 +1256,21 @@ func (checker *Checker) functionType( parameterList *ast.ParameterList, returnTypeAnnotation *ast.TypeAnnotation, ) *FunctionType { + + oldMappedAccess := checker.entitlementMappingInScope + if mapAccess, isMapAccess := access.(*EntitlementMapAccess); isMapAccess { + checker.entitlementMappingInScope = mapAccess.Type + } else { + checker.entitlementMappingInScope = nil + } + defer func() { checker.entitlementMappingInScope = oldMappedAccess }() + convertedParameters := checker.parameters(parameterList) convertedReturnTypeAnnotation := VoidTypeAnnotation if returnTypeAnnotation != nil { - // to allow entitlement mapping types to be used in the return annotation only of - // a mapped accessor function, we introduce a "variable" into the typing scope while - // checking the return - if mapAccess, isMapAccess := access.(*EntitlementMapAccess); isMapAccess { - checker.entitlementMappingInScope = mapAccess.Type - } convertedReturnTypeAnnotation = checker.ConvertTypeAnnotation(returnTypeAnnotation) - checker.entitlementMappingInScope = nil } return &FunctionType{ diff --git a/runtime/tests/checker/entitlements_test.go b/runtime/tests/checker/entitlements_test.go index 727c0f613d..9a7bc9d4c5 100644 --- a/runtime/tests/checker/entitlements_test.go +++ b/runtime/tests/checker/entitlements_test.go @@ -1461,19 +1461,78 @@ func TestCheckBasicEntitlementMappingAccess(t *testing.T) { require.IsType(t, &sema.InvalidMappedEntitlementMemberError{}, errs[0]) }) + t.Run("accessor function with mapped ref arg", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement E + entitlement F + entitlement G + entitlement H + entitlement mapping M { + E -> F + G -> H + } + struct interface S { + access(M) fun foo(_ arg: auth(M) &Int): auth(M) &Int + } + + fun foo(s: auth(E) &{S}) { + s.foo(&1 as auth(F) &Int) + } + `) + + assert.NoError(t, err) + }) + t.Run("accessor function with invalid mapped ref arg", func(t *testing.T) { t.Parallel() _, err := ParseAndCheck(t, ` - entitlement mapping M {} + entitlement E + entitlement F + entitlement G + entitlement H + entitlement mapping M { + E -> F + G -> H + } struct interface S { - access(M) fun foo(arg: auth(M) &Int): auth(M) &Int + access(M) fun foo(_ arg: auth(M) &Int): auth(M) &Int + } + + fun foo(s: auth(E) &{S}) { + s.foo(&1 as auth(H) &Int) } `) errs := RequireCheckerErrors(t, err, 1) - require.IsType(t, &sema.InvalidMappedAuthorizationOutsideOfFieldError{}, errs[0]) + require.IsType(t, &sema.TypeMismatchError{}, errs[0]) + }) + + t.Run("accessor function with full mapped ref arg", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement E + entitlement F + entitlement G + entitlement H + entitlement mapping M { + E -> F + G -> H + } + struct interface S { + access(M) fun foo(_ arg: auth(M) &Int): auth(M) &Int + } + + fun foo(s: {S}) { + s.foo(&1 as auth(F, H) &Int) + } + `) + + assert.NoError(t, err) }) t.Run("multiple mappings conjunction", func(t *testing.T) { @@ -7440,3 +7499,338 @@ func TestInterpretMappingEscalation(t *testing.T) { }) } + +func TestCheckEntitlementMappingComplexFields(t *testing.T) { + + t.Parallel() + + t.Run("array mapped field", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) let arr: [auth(MyMap) &InnerObj] + init() { + self.arr = [&InnerObj()] + } + } + + fun foo() { + let x: auth(Inner1, Inner2) &InnerObj = Carrier().arr[0] + x.first() + x.second() + } + `) + + require.NoError(t, err) + }) + + t.Run("array mapped field via reference", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) let arr: [auth(MyMap) &InnerObj] + init() { + self.arr = [&InnerObj()] + } + } + + fun foo() { + let x = (&Carrier() as auth(Outer1) &Carrier).arr[0] + x.first() // ok + x.second() // fails + } + `) + + errors := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAccessError{}, errors[0]) + }) + + t.Run("array mapped function", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) fun getArr(): [auth(MyMap) &InnerObj] { + return [&InnerObj()] + } + } + + + `) + + errors := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidMappedEntitlementMemberError{}, errors[0]) + }) + + t.Run("array mapped field escape", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) let arr: [auth(MyMap) &InnerObj] + init() { + self.arr = [&InnerObj()] + } + } + + struct TranslatorStruct { + access(self) var carrier: &Carrier; + access(MyMap) fun translate(): auth(MyMap) &InnerObj { + return self.carrier.arr[0] // type mismatch + } + init(_ carrier: &Carrier) { + self.carrier = carrier + } + } + `) + + errors := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.TypeMismatchError{}, errors[0]) + }) + + t.Run("dictionary mapped field", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) let dict: {String: auth(MyMap) &InnerObj} + init() { + self.dict = {"": &InnerObj()} + } + } + + fun foo() { + let x: auth(Inner1, Inner2) &InnerObj = Carrier().dict[""]! + x.first() + x.second() + } + `) + + require.NoError(t, err) + }) + + t.Run("dictionary mapped field via reference", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) let dict: {String: auth(MyMap) &InnerObj} + init() { + self.dict = {"": &InnerObj()} + } + } + + fun foo() { + let x = (&Carrier() as auth(Outer1) &Carrier).dict[""]! + x.first() // ok + x.second() // fails + } + `) + + errors := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidAccessError{}, errors[0]) + }) + + t.Run("array mapped function", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) fun getDict(): {String: auth(MyMap) &InnerObj} { + return {"": &InnerObj()} + } + } + + + `) + + errors := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidMappedEntitlementMemberError{}, errors[0]) + }) + + t.Run("lambda mapped array field", func(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) let fnArr: [fun(auth(MyMap) &InnerObj): auth(MyMap) &InnerObj] + init() { + let innerObj = &InnerObj() as auth(Inner1, Inner2) &InnerObj + self.fnArr = [fun(_ x: &InnerObj): auth(Inner1, Inner2) &InnerObj { + return innerObj + }] + } + + } + + fun foo() { + let x = (&Carrier() as auth(Outer1) &Carrier).fnArr[0] + x(&InnerObj()).first() // ok + + x(&InnerObj() as auth(Inner1) &InnerObj).first() // ok + + x(&InnerObj() as auth(Inner2) &InnerObj).first() // mismatch + + x(&InnerObj()).second() // fails + } + + `) + + errors := RequireCheckerErrors(t, err, 2) + require.IsType(t, &sema.TypeMismatchError{}, errors[0]) + require.IsType(t, &sema.InvalidAccessError{}, errors[1]) + }) + + t.Run("lambda escape", func(t *testing.T) { + + t.Parallel() + + _, err := ParseAndCheck(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct FuncGenerator { + access(MyMap) fun generate(): auth(MyMap) &Int? { + // cannot declare lambda with mapped entitlement + fun innerFunc(_ param: auth(MyMap) &InnerObj): Int { + return 123; + } + var f = innerFunc; // will fail if we're called via a reference + return nil; + } + } + + fun test() { + (&FuncGenerator() as auth(Outer1) &FuncGenerator).generate() + } + `) + + errors := RequireCheckerErrors(t, err, 1) + require.IsType(t, &sema.InvalidMappedAuthorizationOutsideOfFieldError{}, errors[0]) + }) +} diff --git a/runtime/tests/interpreter/entitlements_test.go b/runtime/tests/interpreter/entitlements_test.go index 42e7c57f60..ad6dfeda60 100644 --- a/runtime/tests/interpreter/entitlements_test.go +++ b/runtime/tests/interpreter/entitlements_test.go @@ -2079,6 +2079,83 @@ func TestInterpretEntitlementMappingAccessors(t *testing.T) { ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), ) }) + + t.Run("accessor function with mapped ref arg", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + entitlement E + entitlement F + entitlement G + entitlement H + entitlement mapping M { + E -> F + G -> H + } + struct S { + access(M) fun foo(_ arg: auth(M) &Int): auth(M) &Int { + return arg + } + } + + fun test(): auth(F) &Int { + let s = S() + let sRef = &s as auth(E) &S + return sRef.foo(&1) + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + require.True( + t, + interpreter.NewEntitlementSetAuthorization( + nil, + func() []common.TypeID { return []common.TypeID{"S.test.F"} }, + 1, + sema.Conjunction, + ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), + ) + }) + + t.Run("accessor function with full mapped ref arg", func(t *testing.T) { + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + entitlement E + entitlement F + entitlement G + entitlement H + entitlement mapping M { + E -> F + G -> H + } + struct S { + access(M) fun foo(_ arg: auth(M) &Int): auth(M) &Int { + return arg + } + } + + fun test(): auth(F, H) &Int { + let s = S() + return s.foo(&1) + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + require.True( + t, + interpreter.NewEntitlementSetAuthorization( + nil, + func() []common.TypeID { return []common.TypeID{"S.test.F", "S.test.H"} }, + 2, + sema.Conjunction, + ).Equal(value.(*interpreter.EphemeralReferenceValue).Authorization), + ) + }) } func TestInterpretEntitledAttachments(t *testing.T) { @@ -3280,3 +3357,143 @@ func TestInterpretMappingInclude(t *testing.T) { ) }) } + +func TestInterpretEntitlementMappingComplexFields(t *testing.T) { + t.Parallel() + + t.Run("array field", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) let arr: [auth(MyMap) &InnerObj] + init() { + self.arr = [&InnerObj()] + } + } + + fun test(): Int { + let x = Carrier().arr[0] + return x.first() + x.second() + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredIntValueFromInt64(9999+8888), + value, + ) + }) + + t.Run("dictionary field", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) let dict: {String: auth(MyMap) &InnerObj} + init() { + self.dict = {"": &InnerObj()} + } + } + + fun test(): Int { + let x = Carrier().dict[""]! + return x.first() + x.second() + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredIntValueFromInt64(9999+8888), + value, + ) + }) + + t.Run("lambda array field", func(t *testing.T) { + + t.Parallel() + + inter := parseCheckAndInterpret(t, ` + entitlement Inner1 + entitlement Inner2 + entitlement Outer1 + entitlement Outer2 + + entitlement mapping MyMap { + Outer1 -> Inner1 + Outer2 -> Inner2 + } + struct InnerObj { + access(Inner1) fun first(): Int{ return 9999 } + access(Inner2) fun second(): Int{ return 8888 } + } + + struct Carrier{ + access(MyMap) let fnArr: [fun(auth(MyMap) &InnerObj): auth(MyMap) &InnerObj] + init() { + let innerObj = &InnerObj() as auth(Inner1, Inner2) &InnerObj + self.fnArr = [fun(_ x: &InnerObj): auth(Inner1, Inner2) &InnerObj { + return innerObj + }] + } + + } + + fun test(): Int { + let carrier = Carrier() + let ref1 = &carrier as auth(Outer1) &Carrier + let ref2 = &carrier as auth(Outer2) &Carrier + return ref1.fnArr[0](&InnerObj() as auth(Inner1) &InnerObj).first() + + ref2.fnArr[0](&InnerObj() as auth(Inner2) &InnerObj).second() + } + `) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + AssertValuesEqual( + t, + inter, + interpreter.NewUnmeteredIntValueFromInt64(9999+8888), + value, + ) + }) +}