diff --git a/android/Android.bp b/android/Android.bp index 3c38148b6..774d24a20 100644 --- a/android/Android.bp +++ b/android/Android.bp @@ -42,6 +42,7 @@ bootstrap_go_package { "buildinfo_prop.go", "compliance_metadata.go", "config.go", + "container.go", "test_config.go", "configurable_properties.go", "configured_jars.go", diff --git a/android/container.go b/android/container.go new file mode 100644 index 000000000..c4fdd9c91 --- /dev/null +++ b/android/container.go @@ -0,0 +1,233 @@ +// Copyright 2024 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package android + +import ( + "reflect" + "slices" + + "github.com/google/blueprint" +) + +type StubsAvailableModule interface { + IsStubsModule() bool +} + +// Returns true if the dependency module is a stubs module +var depIsStubsModule = func(_ ModuleContext, _, dep Module) bool { + if stubsModule, ok := dep.(StubsAvailableModule); ok { + return stubsModule.IsStubsModule() + } + return false +} + +// Labels of exception functions, which are used to determine special dependencies that allow +// otherwise restricted inter-container dependencies +type exceptionHandleFuncLabel int + +const ( + checkStubs exceptionHandleFuncLabel = iota +) + +// Functions cannot be used as a value passed in providers, because functions are not +// hashable. As a workaround, the exceptionHandleFunc enum values are passed using providers, +// and the corresponding functions are called from this map. +var exceptionHandleFunctionsTable = map[exceptionHandleFuncLabel]func(ModuleContext, Module, Module) bool{ + checkStubs: depIsStubsModule, +} + +type InstallableModule interface { + EnforceApiContainerChecks() bool +} + +type restriction struct { + // container of the dependency + dependency *container + + // Error message to be emitted to the user when the dependency meets this restriction + errorMessage string + + // List of labels of allowed exception functions that allows bypassing this restriction. + // If any of the functions mapped to each labels returns true, this dependency would be + // considered allowed and an error will not be thrown. + allowedExceptions []exceptionHandleFuncLabel +} +type container struct { + // The name of the container i.e. partition, api domain + name string + + // Map of dependency restricted containers. + restricted []restriction +} + +var ( + VendorContainer = &container{ + name: VendorVariation, + restricted: nil, + } + SystemContainer = &container{ + name: "system", + restricted: []restriction{ + { + dependency: VendorContainer, + errorMessage: "Module belonging to the system partition other than HALs is " + + "not allowed to depend on the vendor partition module, in order to support " + + "independent development/update cycles and to support the Generic System " + + "Image. Try depending on HALs, VNDK or AIDL instead.", + allowedExceptions: []exceptionHandleFuncLabel{}, + }, + }, + } + ProductContainer = &container{ + name: ProductVariation, + restricted: []restriction{ + { + dependency: VendorContainer, + errorMessage: "Module belonging to the product partition is not allowed to " + + "depend on the vendor partition module, as this may lead to security " + + "vulnerabilities. Try depending on the HALs or utilize AIDL instead.", + allowedExceptions: []exceptionHandleFuncLabel{}, + }, + }, + } + ApexContainer = initializeApexContainer() + CtsContainer = &container{ + name: "cts", + restricted: []restriction{ + { + dependency: SystemContainer, + errorMessage: "CTS module should not depend on the modules belonging to the " + + "system partition, including \"framework\". Depending on the system " + + "partition may lead to disclosure of implementation details and regression " + + "due to API changes across platform versions. Try depending on the stubs instead.", + allowedExceptions: []exceptionHandleFuncLabel{checkStubs}, + }, + }, + } +) + +func initializeApexContainer() *container { + apexContainer := &container{ + name: "apex", + restricted: []restriction{ + { + dependency: SystemContainer, + errorMessage: "Module belonging to Apex(es) is not allowed to depend on the " + + "modules belonging to the system partition. Either statically depend on the " + + "module or convert the depending module to java_sdk_library and depend on " + + "the stubs.", + allowedExceptions: []exceptionHandleFuncLabel{checkStubs}, + }, + }, + } + + apexContainer.restricted = append(apexContainer.restricted, restriction{ + dependency: apexContainer, + errorMessage: "Module belonging to Apex(es) is not allowed to depend on the " + + "modules belonging to other Apex(es). Either include the depending " + + "module in the Apex or convert the depending module to java_sdk_library " + + "and depend on its stubs.", + allowedExceptions: []exceptionHandleFuncLabel{checkStubs}, + }) + + return apexContainer +} + +type ContainersInfo struct { + belongingContainers []*container + + belongingApexes []ApexInfo +} + +func (c *ContainersInfo) BelongingContainers() []*container { + return c.belongingContainers +} + +var ContainersInfoProvider = blueprint.NewProvider[ContainersInfo]() + +// Determines if the module can be installed in the system partition or not. +// Logic is identical to that of modulePartition(...) defined in paths.go +func installInSystemPartition(ctx ModuleContext) bool { + module := ctx.Module() + return !module.InstallInTestcases() && + !module.InstallInData() && + !module.InstallInRamdisk() && + !module.InstallInVendorRamdisk() && + !module.InstallInDebugRamdisk() && + !module.InstallInRecovery() && + !module.InstallInVendor() && + !module.InstallInOdm() && + !module.InstallInProduct() && + determineModuleKind(module.base(), ctx.blueprintBaseModuleContext()) == platformModule +} + +func generateContainerInfo(ctx ModuleContext) ContainersInfo { + inSystem := installInSystemPartition(ctx) + inProduct := ctx.Module().InstallInProduct() + inVendor := ctx.Module().InstallInVendor() + inCts := false + inApex := false + + if m, ok := ctx.Module().(ImageInterface); ok { + inProduct = inProduct || m.ProductVariantNeeded(ctx) + inVendor = inVendor || m.VendorVariantNeeded(ctx) + } + + props := ctx.Module().GetProperties() + for _, prop := range props { + val := reflect.ValueOf(prop).Elem() + if val.Kind() == reflect.Struct { + testSuites := val.FieldByName("Test_suites") + if testSuites.IsValid() && testSuites.Kind() == reflect.Slice && slices.Contains(testSuites.Interface().([]string), "cts") { + inCts = true + } + } + } + + var belongingApexes []ApexInfo + if apexInfo, ok := ModuleProvider(ctx, AllApexInfoProvider); ok { + belongingApexes = apexInfo.ApexInfos + inApex = true + } + + containers := []*container{} + if inSystem { + containers = append(containers, SystemContainer) + } + if inProduct { + containers = append(containers, ProductContainer) + } + if inVendor { + containers = append(containers, VendorContainer) + } + if inCts { + containers = append(containers, CtsContainer) + } + if inApex { + containers = append(containers, ApexContainer) + } + + return ContainersInfo{ + belongingContainers: containers, + belongingApexes: belongingApexes, + } +} + +func setContainerInfo(ctx ModuleContext) { + if _, ok := ctx.Module().(InstallableModule); ok { + containersInfo := generateContainerInfo(ctx) + SetProvider(ctx, ContainersInfoProvider, containersInfo) + } +} diff --git a/android/image.go b/android/image.go index c278dcdf9..0f0310701 100644 --- a/android/image.go +++ b/android/image.go @@ -22,7 +22,7 @@ type ImageInterface interface { // VendorVariantNeeded should return true if the module needs a vendor variant (installed on the vendor image). VendorVariantNeeded(ctx BaseModuleContext) bool - // ProductVariantNeeded should return true if the module needs a product variant (unstalled on the product image). + // ProductVariantNeeded should return true if the module needs a product variant (installed on the product image). ProductVariantNeeded(ctx BaseModuleContext) bool // CoreVariantNeeded should return true if the module needs a core variant (installed on the system image). diff --git a/android/module.go b/android/module.go index 9f7cb377d..75e73c678 100644 --- a/android/module.go +++ b/android/module.go @@ -1779,6 +1779,8 @@ func (m *ModuleBase) GenerateBuildActions(blueprintCtx blueprint.ModuleContext) variables: make(map[string]string), } + setContainerInfo(ctx) + m.licenseMetadataFile = PathForModuleOut(ctx, "meta_lic") dependencyInstallFiles, dependencyPackagingSpecs := m.computeInstallDeps(ctx) diff --git a/android/util.go b/android/util.go index e21e66b88..3c0af2f38 100644 --- a/android/util.go +++ b/android/util.go @@ -201,6 +201,12 @@ func ListSetDifference[T comparable](l1, l2 []T) (bool, []T, []T) { return listsDiffer, diff1, diff2 } +// Returns true if the two lists have common elements. +func HasIntersection[T comparable](l1, l2 []T) bool { + _, a, b := ListSetDifference(l1, l2) + return len(a)+len(b) < len(setFromList(l1))+len(setFromList(l2)) +} + // Returns true if the given string s is prefixed with any string in the given prefix list. func HasAnyPrefix(s string, prefixList []string) bool { for _, prefix := range prefixList { diff --git a/android/util_test.go b/android/util_test.go index 8e73d835c..6537d69b9 100644 --- a/android/util_test.go +++ b/android/util_test.go @@ -818,3 +818,52 @@ func TestReverseSlice(t *testing.T) { }) } } + +var hasIntersectionTestCases = []struct { + name string + l1 []string + l2 []string + expected bool +}{ + { + name: "empty", + l1: []string{"a", "b", "c"}, + l2: []string{}, + expected: false, + }, + { + name: "both empty", + l1: []string{}, + l2: []string{}, + expected: false, + }, + { + name: "identical", + l1: []string{"a", "b", "c"}, + l2: []string{"a", "b", "c"}, + expected: true, + }, + { + name: "duplicates", + l1: []string{"a", "a", "a"}, + l2: []string{"a", "b", "c"}, + expected: true, + }, + { + name: "duplicates with no intersection", + l1: []string{"d", "d", "d", "d"}, + l2: []string{"a", "b", "c"}, + expected: false, + }, +} + +func TestHasIntersection(t *testing.T) { + for _, testCase := range hasIntersectionTestCases { + t.Run(testCase.name, func(t *testing.T) { + hasIntersection := HasIntersection(testCase.l1, testCase.l2) + if !reflect.DeepEqual(hasIntersection, testCase.expected) { + t.Errorf("expected %#v, got %#v", testCase.expected, hasIntersection) + } + }) + } +} diff --git a/apex/Android.bp b/apex/Android.bp index abae9e261..17fdfc36a 100644 --- a/apex/Android.bp +++ b/apex/Android.bp @@ -37,6 +37,7 @@ bootstrap_go_package { "apex_test.go", "bootclasspath_fragment_test.go", "classpath_element_test.go", + "container_test.go", "dexpreopt_bootjars_test.go", "platform_bootclasspath_test.go", "systemserver_classpath_fragment_test.go", diff --git a/apex/container_test.go b/apex/container_test.go new file mode 100644 index 000000000..39311741d --- /dev/null +++ b/apex/container_test.go @@ -0,0 +1,329 @@ +// Copyright 2024 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apex + +import ( + "android/soong/android" + "android/soong/java" + "fmt" + "testing" +) + +var checkContainerMatch = func(t *testing.T, name string, container string, expected bool, actual bool) { + errorMessage := fmt.Sprintf("module %s container %s value differ", name, container) + android.AssertBoolEquals(t, errorMessage, expected, actual) +} + +func TestApexDepsContainers(t *testing.T) { + result := android.GroupFixturePreparers( + prepareForApexTest, + java.PrepareForTestWithJavaSdkLibraryFiles, + java.FixtureWithLastReleaseApis("mybootclasspathlib"), + ).RunTestWithBp(t, ` + apex { + name: "myapex", + key: "myapex.key", + bootclasspath_fragments: [ + "mybootclasspathfragment", + ], + updatable: true, + min_sdk_version: "30", + } + apex_key { + name: "myapex.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + bootclasspath_fragment { + name: "mybootclasspathfragment", + contents: [ + "mybootclasspathlib", + ], + apex_available: [ + "myapex", + ], + hidden_api: { + split_packages: ["*"], + }, + } + java_sdk_library { + name: "mybootclasspathlib", + srcs: [ + "mybootclasspathlib.java", + ], + apex_available: [ + "myapex", + ], + compile_dex: true, + static_libs: [ + "foo", + "baz", + ], + libs: [ + "bar", + ], + min_sdk_version: "30", + } + java_library { + name: "foo", + srcs:[ + "A.java", + ], + apex_available: [ + "myapex", + ], + min_sdk_version: "30", + } + java_library { + name: "bar", + srcs:[ + "A.java", + ], + min_sdk_version: "30", + } + java_library { + name: "baz", + srcs:[ + "A.java", + ], + apex_available: [ + "//apex_available:platform", + "myapex", + ], + min_sdk_version: "30", + } + `) + testcases := []struct { + moduleName string + variant string + isSystemContainer bool + isApexContainer bool + }{ + { + moduleName: "mybootclasspathlib", + variant: "android_common_myapex", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "mybootclasspathlib.impl", + variant: "android_common_apex30", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "mybootclasspathlib.stubs", + variant: "android_common", + isSystemContainer: true, + isApexContainer: false, + }, + { + moduleName: "foo", + variant: "android_common_apex30", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "bar", + variant: "android_common", + isSystemContainer: true, + isApexContainer: false, + }, + { + moduleName: "baz", + variant: "android_common_apex30", + isSystemContainer: true, + isApexContainer: true, + }, + } + + for _, c := range testcases { + m := result.ModuleForTests(c.moduleName, c.variant) + containers, _ := android.OtherModuleProvider(result.TestContext.OtherModuleProviderAdaptor(), m.Module(), android.ContainersInfoProvider) + belongingContainers := containers.BelongingContainers() + checkContainerMatch(t, c.moduleName, "system", c.isSystemContainer, android.InList(android.SystemContainer, belongingContainers)) + checkContainerMatch(t, c.moduleName, "apex", c.isApexContainer, android.InList(android.ApexContainer, belongingContainers)) + } +} + +func TestNonUpdatableApexDepsContainers(t *testing.T) { + result := android.GroupFixturePreparers( + prepareForApexTest, + java.PrepareForTestWithJavaSdkLibraryFiles, + java.FixtureWithLastReleaseApis("mybootclasspathlib"), + ).RunTestWithBp(t, ` + apex { + name: "myapex", + key: "myapex.key", + bootclasspath_fragments: [ + "mybootclasspathfragment", + ], + updatable: false, + } + apex_key { + name: "myapex.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + bootclasspath_fragment { + name: "mybootclasspathfragment", + contents: [ + "mybootclasspathlib", + ], + apex_available: [ + "myapex", + ], + hidden_api: { + split_packages: ["*"], + }, + } + java_sdk_library { + name: "mybootclasspathlib", + srcs: [ + "mybootclasspathlib.java", + ], + apex_available: [ + "myapex", + ], + compile_dex: true, + static_libs: [ + "foo", + ], + libs: [ + "bar", + ], + } + java_library { + name: "foo", + srcs:[ + "A.java", + ], + apex_available: [ + "myapex", + ], + } + java_library { + name: "bar", + srcs:[ + "A.java", + ], + } + `) + testcases := []struct { + moduleName string + variant string + isSystemContainer bool + isApexContainer bool + }{ + { + moduleName: "mybootclasspathlib", + variant: "android_common_myapex", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "mybootclasspathlib.impl", + variant: "android_common_apex10000", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "mybootclasspathlib.stubs", + variant: "android_common", + isSystemContainer: true, + isApexContainer: false, + }, + { + moduleName: "foo", + variant: "android_common_apex10000", + isSystemContainer: true, + isApexContainer: true, + }, + { + moduleName: "bar", + variant: "android_common", + isSystemContainer: true, + isApexContainer: false, + }, + } + + for _, c := range testcases { + m := result.ModuleForTests(c.moduleName, c.variant) + containers, _ := android.OtherModuleProvider(result.TestContext.OtherModuleProviderAdaptor(), m.Module(), android.ContainersInfoProvider) + belongingContainers := containers.BelongingContainers() + checkContainerMatch(t, c.moduleName, "system", c.isSystemContainer, android.InList(android.SystemContainer, belongingContainers)) + checkContainerMatch(t, c.moduleName, "apex", c.isApexContainer, android.InList(android.ApexContainer, belongingContainers)) + } +} + +func TestUpdatableAndNonUpdatableApexesIdenticalMinSdkVersion(t *testing.T) { + result := android.GroupFixturePreparers( + prepareForApexTest, + java.PrepareForTestWithJavaSdkLibraryFiles, + android.FixtureMergeMockFs(android.MockFS{ + "system/sepolicy/apex/myapex_non_updatable-file_contexts": nil, + "system/sepolicy/apex/myapex_updatable-file_contexts": nil, + }), + ).RunTestWithBp(t, ` + apex { + name: "myapex_non_updatable", + key: "myapex_non_updatable.key", + java_libs: [ + "foo", + ], + updatable: false, + min_sdk_version: "30", + } + apex_key { + name: "myapex_non_updatable.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + + apex { + name: "myapex_updatable", + key: "myapex_updatable.key", + java_libs: [ + "foo", + ], + updatable: true, + min_sdk_version: "30", + } + apex_key { + name: "myapex_updatable.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + + java_library { + name: "foo", + srcs:[ + "A.java", + ], + apex_available: [ + "myapex_non_updatable", + "myapex_updatable", + ], + min_sdk_version: "30", + sdk_version: "current", + } + `) + + fooApexVariant := result.ModuleForTests("foo", "android_common_apex30") + containers, _ := android.OtherModuleProvider(result.TestContext.OtherModuleProviderAdaptor(), fooApexVariant.Module(), android.ContainersInfoProvider) + belongingContainers := containers.BelongingContainers() + checkContainerMatch(t, "foo", "system", true, android.InList(android.SystemContainer, belongingContainers)) + checkContainerMatch(t, "foo", "apex", true, android.InList(android.ApexContainer, belongingContainers)) +} diff --git a/java/Android.bp b/java/Android.bp index 54b36ab60..a941754db 100644 --- a/java/Android.bp +++ b/java/Android.bp @@ -87,6 +87,7 @@ bootstrap_go_package { "app_set_test.go", "app_test.go", "code_metadata_test.go", + "container_test.go", "bootclasspath_fragment_test.go", "device_host_converter_test.go", "dex_test.go", diff --git a/java/base.go b/java/base.go index fc68d2018..02dc3e35b 100644 --- a/java/base.go +++ b/java/base.go @@ -552,6 +552,18 @@ type Module struct { aconfigCacheFiles android.Paths } +var _ android.InstallableModule = (*Module)(nil) + +// To satisfy the InstallableModule interface +func (j *Module) EnforceApiContainerChecks() bool { + return true +} + +// Overrides android.ModuleBase.InstallInProduct() +func (j *Module) InstallInProduct() bool { + return j.ProductSpecific() +} + func (j *Module) CheckStableSdkVersion(ctx android.BaseModuleContext) error { sdkVersion := j.SdkVersion(ctx) if sdkVersion.Stable() { diff --git a/java/container_test.go b/java/container_test.go new file mode 100644 index 000000000..344185553 --- /dev/null +++ b/java/container_test.go @@ -0,0 +1,129 @@ +// Copyright 2024 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package java + +import ( + "android/soong/android" + "fmt" + "testing" +) + +var checkContainerMatch = func(t *testing.T, name string, container string, expected bool, actual bool) { + errorMessage := fmt.Sprintf("module %s container %s value differ", name, container) + android.AssertBoolEquals(t, errorMessage, expected, actual) +} + +func TestJavaContainersModuleProperties(t *testing.T) { + result := android.GroupFixturePreparers( + prepareForJavaTest, + ).RunTestWithBp(t, ` + java_library { + name: "foo", + srcs: ["A.java"], + } + java_library { + name: "foo_vendor", + srcs: ["A.java"], + vendor: true, + sdk_version: "current", + } + java_library { + name: "foo_soc_specific", + srcs: ["A.java"], + soc_specific: true, + sdk_version: "current", + } + java_library { + name: "foo_product_specific", + srcs: ["A.java"], + product_specific: true, + sdk_version: "current", + } + java_test { + name: "foo_cts_test", + srcs: ["A.java"], + test_suites: [ + "cts", + ], + } + java_test { + name: "foo_non_cts_test", + srcs: ["A.java"], + test_suites: [ + "general-tests", + ], + } + `) + + testcases := []struct { + moduleName string + isSystemContainer bool + isVendorContainer bool + isProductContainer bool + isCts bool + }{ + { + moduleName: "foo", + isSystemContainer: true, + isVendorContainer: false, + isProductContainer: false, + isCts: false, + }, + { + moduleName: "foo_vendor", + isSystemContainer: false, + isVendorContainer: true, + isProductContainer: false, + isCts: false, + }, + { + moduleName: "foo_soc_specific", + isSystemContainer: false, + isVendorContainer: true, + isProductContainer: false, + isCts: false, + }, + { + moduleName: "foo_product_specific", + isSystemContainer: false, + isVendorContainer: false, + isProductContainer: true, + isCts: false, + }, + { + moduleName: "foo_cts_test", + isSystemContainer: false, + isVendorContainer: false, + isProductContainer: false, + isCts: true, + }, + { + moduleName: "foo_non_cts_test", + isSystemContainer: false, + isVendorContainer: false, + isProductContainer: false, + isCts: false, + }, + } + + for _, c := range testcases { + m := result.ModuleForTests(c.moduleName, "android_common") + containers, _ := android.OtherModuleProvider(result.TestContext.OtherModuleProviderAdaptor(), m.Module(), android.ContainersInfoProvider) + belongingContainers := containers.BelongingContainers() + checkContainerMatch(t, c.moduleName, "system", c.isSystemContainer, android.InList(android.SystemContainer, belongingContainers)) + checkContainerMatch(t, c.moduleName, "vendor", c.isVendorContainer, android.InList(android.VendorContainer, belongingContainers)) + checkContainerMatch(t, c.moduleName, "product", c.isProductContainer, android.InList(android.ProductContainer, belongingContainers)) + } +}