diff --git a/.github/schedules/grapheneos-shiba.toml b/.github/schedules/grapheneos-shiba.toml index 56d4d508..9b685f57 100644 --- a/.github/schedules/grapheneos-shiba.toml +++ b/.github/schedules/grapheneos-shiba.toml @@ -17,5 +17,7 @@ MAGISK_PREINIT = "sda10" 'ADDITIONALS[FDROID_PRIVILEGED_EXTENSION]' = false 'ADDITIONALS[BOOT_ANIMATION]' = true 'ADDITIONALS[MAS_COMPATIBLE_SEPOLICY]' = false +# These modules are only supported on LineageOS pdx235, not GrapheneOS. +'ADDITIONALS[EXFATPROGS_FSCK]' = false 'ADDITIONALS[FSCK_SYSFS_SEPOLICY]' = false FORCE_UPDATE = false diff --git a/.github/workflows/build-rom.yml b/.github/workflows/build-rom.yml index 50cee754..2bc04f80 100644 --- a/.github/workflows/build-rom.yml +++ b/.github/workflows/build-rom.yml @@ -191,6 +191,15 @@ jobs: disposable|repository) ;; *) echo "::error::signing-mode must be disposable or repository"; exit 1 ;; esac + # pdx235 filesystem and compiled-policy modifications must never reach GrapheneOS. + # Reject even explicit workflow_call selections before checkout or signing. + if [[ "${ADDITIONALS_EXFATPROGS_FSCK:-false}" == true || + "${ADDITIONALS_FSCK_SYSFS_SEPOLICY:-false}" == true ]]; then + if [[ "${ROM_FAMILY:-}" != lineageos || "${DEVICE_NAME:-}" != pdx235 ]]; then + echo "::error::pdx235 fsck.exfat/SELinux modules require LineageOS pdx235" + exit 1 + fi + fi root_mode="${ROOT_MODE_INPUT}" if [[ -z "${root_mode}" ]]; then if [[ "${ADDITIONALS_ROOT}" == "true" ]]; then diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24e63858..ec4cf19f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -372,6 +372,9 @@ jobs: disable-system-updater: ${{ github.event_name == 'schedule' && needs.preflight.outputs.disable_system_updater == 'true' || (github.event_name != 'schedule' && inputs.disable-system-updater) }} fdroid-privileged-extension: ${{ github.event_name == 'schedule' && needs.preflight.outputs.fdroid_privileged_extension == 'true' || (github.event_name != 'schedule' && inputs.fdroid-privileged-extension) }} shizuku: ${{ github.event_name != 'schedule' && inputs.shizuku || false }} + # LineageOS pdx235-only modules; never forward them from a GrapheneOS trigger. + exfatprogs-fsck: false + fsck-sysfs-sepolicy: false compatible-sepolicy-patching: ${{ github.event_name == 'schedule' && needs.preflight.outputs.compatible_sepolicy_patching == 'true' || github.event_name != 'schedule' && inputs.compatible-sepolicy-patching || false }} allow-unauthorized-adb: ${{ github.event_name != 'schedule' && inputs.allow-unauthorized-adb || false }} boot-animation: ${{ github.event_name == 'schedule' && needs.preflight.outputs.boot_animation == 'true' || github.event_name != 'schedule' && inputs.boot-animation }} diff --git a/src/ci/load_schedule_definition.sh b/src/ci/load_schedule_definition.sh index 27a850e7..cf14b658 100644 --- a/src/ci/load_schedule_definition.sh +++ b/src/ci/load_schedule_definition.sh @@ -73,6 +73,15 @@ if [[ -n "${EXPECTED_ROM_FAMILY:-}" && "${rom_family}" != "${EXPECTED_ROM_FAMILY exit 1 fi +# Fail before exporting a scheduled selection: pdx235-only modules must not +# leak into GrapheneOS or other devices, even if a TOML entry is changed. +if [[ "${exfatprogs_fsck}" != false || "${fsck_sysfs_sepolicy}" != false ]]; then + if [[ "${rom_family}" != lineageos || "${device_id}" != pdx235 ]]; then + echo "::error::exfatprogs fsck and fsck sysfs SELinux are restricted to LineageOS pdx235." >&2 + exit 1 + fi +fi + case "${root_mode}" in rootless|magisk|both) ;; *) diff --git a/tests/rom_contract_test.sh b/tests/rom_contract_test.sh index aaad079c..8ce7ee2f 100644 --- a/tests/rom_contract_test.sh +++ b/tests/rom_contract_test.sh @@ -333,6 +333,29 @@ test_fsck_sysfs_sepolicy_is_pdx235_scoped_and_fingerprinted() ( fi ) +# GrapheneOS builds must fail closed even if a caller explicitly requests +# a pdx235-only patch through the reusable workflow or local configuration. +test_grapheneos_rejects_pdx235_fsck_modules() ( + local device module + + load_contract + set_selection_fixture + ROM_FAMILY="grapheneos" + ADDITIONALS[EXFATPROGS_FSCK]="false" + ADDITIONALS[FSCK_SYSFS_SEPOLICY]="false" + + for device in shiba pdx235; do + DEVICE_NAME="${device}" + for module in EXFATPROGS_FSCK FSCK_SYSFS_SEPOLICY; do + ADDITIONALS["${module}"]="true" + if module_selection_fingerprint >/dev/null 2>&1; then + fail "GrapheneOS/${device} unexpectedly accepted pdx235-only ${module}" + fi + ADDITIONALS["${module}"]="false" + done + done +) + test_output_filename_contains_fingerprint() ( local full @@ -458,6 +481,7 @@ test_unknown_rom_fails_closed test_invalid_compatible_sepolicy_fails_closed test_selection_fingerprint test_fsck_sysfs_sepolicy_is_pdx235_scoped_and_fingerprinted +test_grapheneos_rejects_pdx235_fsck_modules test_output_filename_contains_fingerprint test_output_policy diff --git a/tests/scheduled_build_definitions_test.sh b/tests/scheduled_build_definitions_test.sh index c645f4d5..1f4c951f 100644 --- a/tests/scheduled_build_definitions_test.sh +++ b/tests/scheduled_build_definitions_test.sh @@ -39,6 +39,8 @@ check_definition() { fail "${file}: wrong fsck sysfs-SEPolicy selection" local expected_exfat=false [[ "${expected_family}" != lineageos ]] || expected_exfat=true + toml_config_has exfatprogs_fsck || + fail "${file}: exfatprogs fsck selection must be explicitly declared" [[ "$(toml_resolve_value exfatprogs_fsck false)" == "${expected_exfat}" ]] || fail "${file}: wrong exfatprogs fsck selection" [[ "$(toml_resolve_value force_update '')" == false ]] || @@ -118,6 +120,47 @@ check_loader() { check_loader .github/schedules/grapheneos-shiba.toml shiba grapheneos check_loader .github/schedules/lineageos-pdx235.toml pdx235 lineageos +# The loader must reject a tampered GrapheneOS schedule before exporting any +# build inputs. Test each forbidden switch using an otherwise-valid definition. +check_forbidden_grapheneos_module() { + local module="${1}" fixture output_file env_file error_file + local old="'ADDITIONALS[${module}]' = false" + local new="'ADDITIONALS[${module}]' = true" + fixture="$(mktemp --suffix=.toml)" + output_file="$(mktemp)" + env_file="$(mktemp)" + error_file="$(mktemp)" + cp .github/schedules/grapheneos-shiba.toml "${fixture}" + check_loader "${fixture}" shiba grapheneos + grep -Fxq "${old}" "${fixture}" || fail "missing GrapheneOS baseline: ${module}" + sed -i "s/^'ADDITIONALS\\[${module}\\]' = false$/'ADDITIONALS[${module}]' = true/" "${fixture}" + grep -Fxq "${new}" "${fixture}" || fail "could not activate ${module} in fixture" + + if GITHUB_OUTPUT="${output_file}" GITHUB_ENV="${env_file}" \ + SCHEDULE_DEFINITION="${fixture}" EXPECTED_ROM_FAMILY=grapheneos \ + bash src/ci/load_schedule_definition.sh >/dev/null 2>"${error_file}"; then + fail "GrapheneOS schedule unexpectedly accepted ${module}" + fi + grep -Fq 'restricted to LineageOS pdx235' "${error_file}" || + fail "GrapheneOS ${module} failure did not come from the device-scope guard" + [[ ! -s "${output_file}" && ! -s "${env_file}" ]] || + fail "GrapheneOS ${module} leaked forbidden selection into scheduled build outputs" + rm -f "${fixture}" "${output_file}" "${env_file}" "${error_file}" +} + +check_forbidden_grapheneos_module EXFATPROGS_FSCK +check_forbidden_grapheneos_module FSCK_SYSFS_SEPOLICY + +# Both GrapheneOS entry points use this shared explicit-false callsite. +for module in exfatprogs-fsck fsck-sysfs-sepolicy; do + grep -Eq "^[[:space:]]{6}${module}:[[:space:]]*false[[:space:]]*$" \ + .github/workflows/release.yml || + fail "GrapheneOS trigger must explicitly disable ${module}" +done +grep -Fq 'pdx235 fsck.exfat/SELinux modules require LineageOS pdx235' \ + .github/workflows/build-rom.yml || + fail "Shared workflow must reject non-pdx235 fsck/SELinux selections early" + grep -Fq 'SCHEDULE_DEFINITION: .github/schedules/grapheneos-shiba.toml' .github/workflows/release.yml || fail "GrapheneOS cron does not reference the shiba schedule definition" grep -Fq 'SCHEDULE_DEFINITION: .github/schedules/lineageos-pdx235.toml' .github/workflows/release-lineage.yml || diff --git a/tests/workflow_contract_test.sh b/tests/workflow_contract_test.sh index 3a1beeb5..484e9fc8 100644 --- a/tests/workflow_contract_test.sh +++ b/tests/workflow_contract_test.sh @@ -386,6 +386,27 @@ with tempfile.TemporaryDirectory() as scratch: ('PUBLISH_REQUEST', 'true')]: assert subprocess.run(['bash', '-euc', script], env={**base, name: forbidden}, capture_output=True).returncode != 0, name + + # Exercise the exact reusable-build request gate, including the forbidden + # case where the device ID is pdx235 but the ROM is GrapheneOS. + normal = {**base, 'EXPORT_DEVICE_CANDIDATE': 'false', + 'DEVELOPMENT_PROFILE': 'none'} + for module in ('ADDITIONALS_EXFATPROGS_FSCK', 'ADDITIONALS_FSCK_SYSFS_SEPOLICY'): + for family, device in (('grapheneos', 'shiba'), ('grapheneos', 'pdx235'), + ('lineageos', 'shiba')): + rejected = subprocess.run( + ['bash', '-euc', script], + env={**normal, 'ROM_FAMILY': family, 'DEVICE_NAME': device, module: 'true'}, + capture_output=True) + assert rejected.returncode != 0, (module, family, device) + assert b'fsck.exfat/SELinux modules require LineageOS pdx235' in rejected.stdout + + allowed = subprocess.run( + ['bash', '-euc', script], + env={**normal, 'ROM_FAMILY': 'lineageos', 'DEVICE_NAME': 'pdx235', + module: 'true'}, + capture_output=True) + assert allowed.returncode == 0, (module, allowed.stderr) # The one artifact destination is the export script's fresh allowlisted stage, # after normal patch/postflight and metadata steps; no working-directory glob. upload = workflow.split(' - name: Upload device candidate\n', 1)[1].split(' - name:', 1)[0]