Skip to content
2 changes: 2 additions & 0 deletions .github/schedules/grapheneos-shiba.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 9 additions & 0 deletions .github/workflows/build-rom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
9 changes: 9 additions & 0 deletions src/ci/load_schedule_definition.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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) ;;
*)
Expand Down
24 changes: 24 additions & 0 deletions tests/rom_contract_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
43 changes: 43 additions & 0 deletions tests/scheduled_build_definitions_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]] ||
Expand Down Expand Up @@ -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 ||
Expand Down
21 changes: 21 additions & 0 deletions tests/workflow_contract_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading