Relax freeze_test to check only compatibility

For now, freeze_test compares prebuilts against sources with diff, to
ensure that sources are identical to prebuilts. However, it could be the
case that the branch should be able to build both REL and ToT. In that
case, changes to the sources are inevitable and the freeze test will
fail.

To fix the issue, freeze_test will now only check compatibility. To be
specific, it will check if any public types or attributes are removed.
Contexts files and neverallow rules are not checked, but they may be
added later. Also to support the new freeze_test

- build_files module is changed to use glob (because REL version won't
  be in compat versions list)
- plat_pub_policy modules are added under prebuilts/api (because
  freeze_test needs that)

Bug: 296875906
Test: m selinux_policy
Change-Id: I39c40992965b98664facea3b760d9d6be1f6b87e
diff --git a/Android.bp b/Android.bp
index b0544cf..dedcf63 100644
--- a/Android.bp
+++ b/Android.bp
@@ -913,7 +913,7 @@
 // SEPOLICY_FREEZE_TEST_EXTRA_DIRS and SEPOLICY_FREEZE_TEST_EXTRA_PREBUILT_DIRS.
 //////////////////////////////////
 se_freeze_test {
-    name: "sepolicy_freeze_test",
+    name: "se_freeze_test",
 }
 
 //////////////////////////////////
diff --git a/build/soong/build_files.go b/build/soong/build_files.go
index db028e6..a15c65c 100644
--- a/build/soong/build_files.go
+++ b/build/soong/build_files.go
@@ -16,6 +16,7 @@
 
 import (
 	"fmt"
+	"path"
 	"path/filepath"
 	"strings"
 
@@ -103,8 +104,15 @@
 	b.srcs[".vendor"] = b.findSrcsInDirs(ctx, ctx.DeviceConfig().VendorSepolicyDirs()...)
 	b.srcs[".odm"] = b.findSrcsInDirs(ctx, ctx.DeviceConfig().OdmSepolicyDirs()...)
 
+	prebuilt_directories, err := ctx.GlobWithDeps("system/sepolicy/prebuilts/api/*", nil)
+	if err != nil {
+		ctx.ModuleErrorf("error while globbing: %w", err)
+		return
+	}
+
 	// directories used for compat tests and Treble tests
-	for _, ver := range ctx.DeviceConfig().PlatformSepolicyCompatVersions() {
+	for _, dir := range prebuilt_directories {
+		ver := path.Base(dir)
 		b.srcs[".plat_public_"+ver] = b.findSrcsInDirs(ctx, filepath.Join("system", "sepolicy", "prebuilts", "api", ver, "public"))
 		b.srcs[".plat_private_"+ver] = b.findSrcsInDirs(ctx, filepath.Join("system", "sepolicy", "prebuilts", "api", ver, "private"))
 		b.srcs[".system_ext_public_"+ver] = b.findSrcsInDirs(ctx, filepath.Join(ctx.DeviceConfig().SystemExtSepolicyPrebuiltApiDir(), "prebuilts", "api", ver, "public"))
diff --git a/build/soong/sepolicy_freeze.go b/build/soong/sepolicy_freeze.go
index 9ae7826..385d6af 100644
--- a/build/soong/sepolicy_freeze.go
+++ b/build/soong/sepolicy_freeze.go
@@ -15,12 +15,14 @@
 package selinux
 
 import (
-	"path/filepath"
 	"sort"
 
 	"android/soong/android"
 )
 
+var currentCilTag = dependencyTag{name: "current_cil"}
+var prebuiltCilTag = dependencyTag{name: "prebuilt_cil"}
+
 func init() {
 	ctx := android.InitRegistrationContext
 	ctx.RegisterParallelSingletonModuleType("se_freeze_test", freezeTestFactory)
@@ -32,6 +34,9 @@
 func freezeTestFactory() android.SingletonModule {
 	f := &freezeTestModule{}
 	android.InitAndroidModule(f)
+	android.AddLoadHook(f, func(ctx android.LoadHookContext) {
+		f.loadHook(ctx)
+	})
 	return f
 }
 
@@ -40,26 +45,23 @@
 	freezeTestTimestamp android.ModuleOutPath
 }
 
-func (f *freezeTestModule) GenerateSingletonBuildActions(ctx android.SingletonContext) {
-	// does nothing; se_freeze_test is a singeton because two freeze test modules don't make sense.
-}
-
-func (f *freezeTestModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+func (f *freezeTestModule) shouldSkip(ctx android.EarlyModuleContext) bool {
 	platformVersion := ctx.DeviceConfig().PlatformSepolicyVersion()
 	totVersion := ctx.DeviceConfig().TotSepolicyVersion()
 
+	return platformVersion == totVersion
+}
+
+func (f *freezeTestModule) loadHook(ctx android.LoadHookContext) {
 	extraDirs := ctx.DeviceConfig().SepolicyFreezeTestExtraDirs()
 	extraPrebuiltDirs := ctx.DeviceConfig().SepolicyFreezeTestExtraPrebuiltDirs()
-	f.freezeTestTimestamp = android.PathForModuleOut(ctx, "freeze_test")
 
-	if platformVersion == totVersion {
+	if f.shouldSkip(ctx) {
 		if len(extraDirs) > 0 || len(extraPrebuiltDirs) > 0 {
 			ctx.ModuleErrorf("SEPOLICY_FREEZE_TEST_EXTRA_DIRS or SEPOLICY_FREEZE_TEST_EXTRA_PREBUILT_DIRS cannot be set before system/sepolicy freezes.")
 			return
 		}
 
-		// we still build a rule to prevent possible regression
-		android.WriteFileRule(ctx, f.freezeTestTimestamp, ";; no freeze tests needed before system/sepolicy freezes")
 		return
 	}
 
@@ -67,17 +69,80 @@
 		ctx.ModuleErrorf("SEPOLICY_FREEZE_TEST_EXTRA_DIRS and SEPOLICY_FREEZE_TEST_EXTRA_PREBUILT_DIRS must have the same number of directories.")
 		return
 	}
+}
 
-	platPublic := filepath.Join(ctx.ModuleDir(), "public")
-	platPrivate := filepath.Join(ctx.ModuleDir(), "private")
-	prebuiltPublic := filepath.Join(ctx.ModuleDir(), "prebuilts", "api", platformVersion, "public")
-	prebuiltPrivate := filepath.Join(ctx.ModuleDir(), "prebuilts", "api", platformVersion, "private")
+func (f *freezeTestModule) prebuiltCilModuleName(ctx android.EarlyModuleContext) string {
+	return ctx.DeviceConfig().PlatformSepolicyVersion() + "_plat_pub_policy.cil"
+}
 
-	sourceDirs := append(extraDirs, platPublic, platPrivate)
-	prebuiltDirs := append(extraPrebuiltDirs, prebuiltPublic, prebuiltPrivate)
+func (f *freezeTestModule) DepsMutator(ctx android.BottomUpMutatorContext) {
+	if f.shouldSkip(ctx) {
+		return
+	}
+
+	ctx.AddDependency(f, currentCilTag, "base_plat_pub_policy.cil")
+	ctx.AddDependency(f, prebuiltCilTag, f.prebuiltCilModuleName(ctx))
+}
+
+func (f *freezeTestModule) GenerateSingletonBuildActions(ctx android.SingletonContext) {
+	// does nothing; se_freeze_test is a singeton because two freeze test modules don't make sense.
+}
+
+func (f *freezeTestModule) outputFileOfDep(ctx android.ModuleContext, depTag dependencyTag) android.Path {
+	deps := ctx.GetDirectDepsWithTag(depTag)
+	if len(deps) != 1 {
+		ctx.ModuleErrorf("%d deps having tag %q; expected only one dep", len(deps), depTag)
+		return nil
+	}
+
+	dep := deps[0]
+	outputFileProducer, ok := dep.(android.OutputFileProducer)
+	if !ok {
+		ctx.ModuleErrorf("module %q is not an output file producer", dep.String())
+		return nil
+	}
+
+	output, err := outputFileProducer.OutputFiles("")
+	if err != nil {
+		ctx.ModuleErrorf("module %q failed to produce output: %w", dep.String(), err)
+		return nil
+	}
+	if len(output) != 1 {
+		ctx.ModuleErrorf("module %q produced %d outputs; expected only one output", dep.String(), len(output))
+		return nil
+	}
+
+	return output[0]
+}
+
+func (f *freezeTestModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	f.freezeTestTimestamp = android.PathForModuleOut(ctx, "freeze_test")
+
+	if f.shouldSkip(ctx) {
+		// we still build a rule to prevent possible regression
+		android.WriteFileRule(ctx, f.freezeTestTimestamp, ";; no freeze tests needed before system/sepolicy freezes")
+		return
+	}
+
+	// Freeze test 1: compare ToT sepolicy and prebuilt sepolicy
+	currentCil := f.outputFileOfDep(ctx, currentCilTag)
+	prebuiltCil := f.outputFileOfDep(ctx, prebuiltCilTag)
+	if ctx.Failed() {
+		return
+	}
+
+	rule := android.NewRuleBuilder(pctx, ctx)
+	rule.Command().BuiltTool("sepolicy_freeze_test").
+		FlagWithInput("-c ", currentCil).
+		FlagWithInput("-p ", prebuiltCil)
+
+	// Freeze test 2: compare extra directories
+	// We don't know the exact structure of extra directories, so just directly compare them
+	extraDirs := ctx.DeviceConfig().SepolicyFreezeTestExtraDirs()
+	extraPrebuiltDirs := ctx.DeviceConfig().SepolicyFreezeTestExtraPrebuiltDirs()
 
 	var implicits []string
-	for _, dir := range append(sourceDirs, prebuiltDirs...) {
+	for _, dir := range append(extraDirs, extraPrebuiltDirs...) {
 		glob, err := ctx.GlobWithDeps(dir+"/**/*", []string{"bug_map"} /* exclude */)
 		if err != nil {
 			ctx.ModuleErrorf("failed to glob sepolicy dir %q: %s", dir, err.Error())
@@ -87,15 +152,13 @@
 	}
 	sort.Strings(implicits)
 
-	rule := android.NewRuleBuilder(pctx, ctx)
-
-	for idx, _ := range sourceDirs {
+	for idx, _ := range extraDirs {
 		rule.Command().Text("diff").
 			Flag("-r").
 			Flag("-q").
 			FlagWithArg("-x ", "bug_map"). // exclude
-			Text(sourceDirs[idx]).
-			Text(prebuiltDirs[idx])
+			Text(extraDirs[idx]).
+			Text(extraPrebuiltDirs[idx])
 	}
 
 	rule.Command().Text("touch").
diff --git a/prebuilts/api/29.0/Android.bp b/prebuilts/api/29.0/Android.bp
new file mode 100644
index 0000000..34052a2
--- /dev/null
+++ b/prebuilts/api/29.0/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+se_policy_conf {
+    name: "29.0_plat_pub_policy.conf",
+    srcs: [":se_build_files{.plat_public_29.0}", ":se_build_files{.reqd_mask}"],
+    installable: false,
+    build_variant: "user",
+}
+
+se_policy_cil {
+    name: "29.0_plat_pub_policy.cil",
+    src: ":29.0_plat_pub_policy.conf",
+    filter_out: [":reqd_policy_mask.cil"],
+    secilc_check: false,
+    installable: false,
+}
diff --git a/prebuilts/api/30.0/Android.bp b/prebuilts/api/30.0/Android.bp
new file mode 100644
index 0000000..0e51bb7
--- /dev/null
+++ b/prebuilts/api/30.0/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+se_policy_conf {
+    name: "30.0_plat_pub_policy.conf",
+    srcs: [":se_build_files{.plat_public_30.0}", ":se_build_files{.reqd_mask}"],
+    installable: false,
+    build_variant: "user",
+}
+
+se_policy_cil {
+    name: "30.0_plat_pub_policy.cil",
+    src: ":30.0_plat_pub_policy.conf",
+    filter_out: [":reqd_policy_mask.cil"],
+    secilc_check: false,
+    installable: false,
+}
diff --git a/prebuilts/api/31.0/Android.bp b/prebuilts/api/31.0/Android.bp
new file mode 100644
index 0000000..5697173
--- /dev/null
+++ b/prebuilts/api/31.0/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+se_policy_conf {
+    name: "31.0_plat_pub_policy.conf",
+    srcs: [":se_build_files{.plat_public_31.0}", ":se_build_files{.reqd_mask}"],
+    installable: false,
+    build_variant: "user",
+}
+
+se_policy_cil {
+    name: "31.0_plat_pub_policy.cil",
+    src: ":31.0_plat_pub_policy.conf",
+    filter_out: [":reqd_policy_mask.cil"],
+    secilc_check: false,
+    installable: false,
+}
diff --git a/prebuilts/api/32.0/Android.bp b/prebuilts/api/32.0/Android.bp
new file mode 100644
index 0000000..723f743
--- /dev/null
+++ b/prebuilts/api/32.0/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+se_policy_conf {
+    name: "32.0_plat_pub_policy.conf",
+    srcs: [":se_build_files{.plat_public_32.0}", ":se_build_files{.reqd_mask}"],
+    installable: false,
+    build_variant: "user",
+}
+
+se_policy_cil {
+    name: "32.0_plat_pub_policy.cil",
+    src: ":32.0_plat_pub_policy.conf",
+    filter_out: [":reqd_policy_mask.cil"],
+    secilc_check: false,
+    installable: false,
+}
diff --git a/prebuilts/api/33.0/Android.bp b/prebuilts/api/33.0/Android.bp
new file mode 100644
index 0000000..df4da89
--- /dev/null
+++ b/prebuilts/api/33.0/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+se_policy_conf {
+    name: "33.0_plat_pub_policy.conf",
+    srcs: [":se_build_files{.plat_public_33.0}", ":se_build_files{.reqd_mask}"],
+    installable: false,
+    build_variant: "user",
+}
+
+se_policy_cil {
+    name: "33.0_plat_pub_policy.cil",
+    src: ":33.0_plat_pub_policy.conf",
+    filter_out: [":reqd_policy_mask.cil"],
+    secilc_check: false,
+    installable: false,
+}
diff --git a/prebuilts/api/34.0/Android.bp b/prebuilts/api/34.0/Android.bp
new file mode 100644
index 0000000..e962848
--- /dev/null
+++ b/prebuilts/api/34.0/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// 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.
+
+se_policy_conf {
+    name: "34.0_plat_pub_policy.conf",
+    srcs: [":se_build_files{.plat_public_34.0}", ":se_build_files{.reqd_mask}"],
+    installable: false,
+    build_variant: "user",
+}
+
+se_policy_cil {
+    name: "34.0_plat_pub_policy.cil",
+    src: ":34.0_plat_pub_policy.conf",
+    filter_out: [":reqd_policy_mask.cil"],
+    secilc_check: false,
+    installable: false,
+}
diff --git a/tests/Android.bp b/tests/Android.bp
index cc0bdc1..544db37 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -146,3 +146,18 @@
     name: "check_prop_prefix",
     srcs: ["check_prop_prefix.py"],
 }
+
+python_binary_host {
+    name: "sepolicy_freeze_test",
+    srcs: [
+        "sepolicy_freeze_test.py",
+    ],
+    version: {
+        py3: {
+            embedded_launcher: true,
+        },
+    },
+    libs: [
+        "mini_cil_parser",
+    ],
+}
diff --git a/tests/sepolicy_freeze_test.py b/tests/sepolicy_freeze_test.py
new file mode 100644
index 0000000..72c8fde
--- /dev/null
+++ b/tests/sepolicy_freeze_test.py
@@ -0,0 +1,56 @@
+# Copyright 2023 The Android Open Source Project
+#
+# 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.
+
+from optparse import OptionParser
+import mini_parser
+import os
+import sys
+
+def do_main():
+    usage = "sepolicy_freeze_test "
+    usage += "-c current_cil -p prebuilt_cil [--help]"
+    parser = OptionParser(usage=usage)
+    parser.add_option("-c", "--current", dest="current", metavar="FILE")
+    parser.add_option("-p", "--prebuilt", dest="prebuilt", metavar="FILE")
+
+    (options, args) = parser.parse_args()
+
+    if not options.current or not options.prebuilt:
+        sys.exit("Must specify both current and prebuilt\n" + parser.usage)
+    if not os.path.exists(options.current):
+        sys.exit("Current policy " + options.current + " does not exist\n"
+                + parser.usage)
+    if not os.path.exists(options.prebuilt):
+        sys.exit("Prebuilt policy " + options.prebuilt + " does not exist\n"
+                + parser.usage)
+
+    current_policy = mini_parser.MiniCilParser(options.current)
+    prebuilt_policy = mini_parser.MiniCilParser(options.prebuilt)
+
+    results = ""
+    removed_types = prebuilt_policy.types - current_policy.types
+    removed_attributes = prebuilt_policy.typeattributes - current_policy.typeattributes
+    removed_attributes = set(filter(lambda x: "base_typeattr_" not in x, removed_attributes))
+
+    if removed_types:
+        results += "The following public types were removed:\n" + ", ".join(removed_types) + "\n"
+
+    if removed_attributes:
+        results += "The following public attributes were removed:\n" + ", ".join(removed_attributes) + "\n"
+
+    if len(results) > 0:
+        sys.exit(results)
+
+if __name__ == '__main__':
+    do_main()