Files
InstanceBrick--nightcap/scripts/auto_update_roblox.sh
T

1679 lines
66 KiB
Bash
Executable File

#!/usr/bin/env bash
# Copyright 2026 Mocktail Project Authors
#
# 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
set -Eeuo pipefail
umask 077
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
DATA_ROOT="${MOCKTAIL_DATA_ROOT:-${XDG_DATA_HOME:-${HOME}/.local/share}/mocktail}"
CACHE_ROOT="${MOCKTAIL_CACHE_ROOT:-${XDG_CACHE_HOME:-${HOME}/.cache}/mocktail}"
STATE_ROOT="${MOCKTAIL_STATE_ROOT:-${XDG_STATE_HOME:-${HOME}/.local/state}/mocktail}"
DEFAULT_METADATA_ROOT="${PROJECT_ROOT}/config"
if [[ -d "${PROJECT_ROOT}/metadata" ]]; then
DEFAULT_METADATA_ROOT="${PROJECT_ROOT}/metadata"
fi
COMPATIBILITY_PATH="${MOCKTAIL_UPDATE_COMPATIBILITY_PATH:-${DEFAULT_METADATA_ROOT}/roblox_compatibility.json}"
BOOTSTRAP_SOURCES_PATH="${MOCKTAIL_BOOTSTRAP_SOURCES_PATH:-${DEFAULT_METADATA_ROOT}/roblox_bootstrap_sources.json}"
SIGNING_TRUST_PATH="${MOCKTAIL_UPDATE_SIGNING_TRUST_PATH:-${DEFAULT_METADATA_ROOT}/roblox_signing_certificates.json}"
# Child validators/providers must use the same resolved source/install layout.
# In an installed tree PROJECT_ROOT contains metadata/, never config/.
export MOCKTAIL_UPDATE_COMPATIBILITY_PATH="${COMPATIBILITY_PATH}"
export MOCKTAIL_BOOTSTRAP_SOURCES_PATH="${BOOTSTRAP_SOURCES_PATH}"
export MOCKTAIL_UPDATE_SIGNING_TRUST_PATH="${SIGNING_TRUST_PATH}"
FETCH_SCRIPT="${MOCKTAIL_UPDATE_FETCH_SCRIPT:-${SCRIPT_DIR}/fetch_roblox_apks.sh}"
VALIDATOR_SCRIPT="${MOCKTAIL_UPDATE_VALIDATOR_SCRIPT:-${SCRIPT_DIR}/update_roblox_payload.sh}"
PAYLOAD_STORE_SCRIPT="${MOCKTAIL_UPDATE_STORE_SCRIPT:-${SCRIPT_DIR}/payload_store.sh}"
SMOKE_SCRIPT="${MOCKTAIL_UPDATE_SMOKE_SCRIPT:-${PROJECT_ROOT}/scripts/real_bringup_smoke.sh}"
PROFILE_DERIVER="${MOCKTAIL_UPDATE_PROFILE_DERIVER:-${SCRIPT_DIR}/derive_roblox_host_abi_profile.py}"
PACKAGED_CANARY_BINARY=""
if [[ "${DEFAULT_METADATA_ROOT}" == "${PROJECT_ROOT}/metadata" ]]; then
packaged_prefix=""
if [[ "${PROJECT_ROOT}" == */share/mocktail ]]; then
packaged_prefix="${PROJECT_ROOT%/share/mocktail}"
fi
for packaged_candidate in \
"${PROJECT_ROOT}/bin/mocktail" \
"${packaged_prefix:+${packaged_prefix}/lib/mocktail/mocktail}" \
"${packaged_prefix:+${packaged_prefix}/bin/mocktail}"; do
if [[ -n "${packaged_candidate}" && -f "${packaged_candidate}" &&
! -L "${packaged_candidate}" && -x "${packaged_candidate}" ]]; then
PACKAGED_CANARY_BINARY="${packaged_candidate}"
break
fi
done
fi
if [[ -n "${MOCKTAIL_CONFIG_FILE:-}" ]]; then
CONFIG_FILE="${MOCKTAIL_CONFIG_FILE}"
elif [[ -n "${MOCKTAIL_CONFIG_ROOT:-}" ]]; then
CONFIG_FILE="${MOCKTAIL_CONFIG_ROOT}/config.yaml"
else
CONFIG_FILE="${XDG_CONFIG_HOME:-${HOME}/.config}/mocktail/config.yaml"
fi
PROVIDER_DIR="${SCRIPT_DIR}/apk_providers"
VERSION_CHECK_COMMAND="${MOCKTAIL_UPDATE_CHECK_SCRIPT:-}"
SOURCE="${MOCKTAIL_UPDATE_SOURCE:-}"
VERSION=""
EXPECTED_PAYLOAD_VERSION_CODE=""
CANARY_ROOT=""
CANARY_LOG=""
PROBATION_ROOT=""
CANDIDATE_PROFILE=""
CANDIDATE_COMPATIBILITY=""
CANDIDATE_IS_PROBATION=false
CANARY_ATTESTATIONS=()
CANARY_BINARY=""
RUNTIME_FINGERPRINT=""
RUNTIME_BUILD_ID=""
LATEST_STAGED_PAYLOAD_ID=""
LAST_ATTEMPT_BASELINE_PAYLOAD_ID="none"
BOOTSTRAP_SELECTED_VERSION_CODE=""
CANDIDATE_NEEDS_REFERENCE=false
CANDIDATE_REJECTION_DEFERRED=false
CANDIDATE_REJECTION_KEY=""
CANDIDATE_REJECTION_PROFILE_SHA256=""
CANDIDATE_REJECTION_COMPATIBILITY_SHA256=""
CANDIDATE_REJECTION_REFERENCE_SHA256=""
CANDIDATE_REJECTION_DERIVER_SHA256=""
CANDIDATE_REJECTION_SCHEMA_VERSION=""
LAUNCH="${MOCKTAIL_UPDATE_LAUNCH:-false}"
SKIP_BUILD=false
SCHEDULED=false
STARTUP_PREFLIGHT=false
UPDATE_CONFIG_JSON="{}"
STAGED_PAYLOAD_ID=""
DOWNLOAD_STAGING_ROOT="${CACHE_ROOT}/downloads/apk-staging"
DOWNLOADED_BUNDLE_DIR=""
UPDATER_LOG=""
SUPPORT_BUNDLE_COLLECTED=false
SUPPORT_CANDIDATE_METADATA=""
SUPPORT_SOURCE_LOG=""
SUPPORT_FAILURE_REASON="updater-startup-failed"
Usage() {
cat <<'EOF'
Usage: scripts/auto_update_roblox.sh [OPTIONS]
Discover and stage latest Roblox x86_64, then activate it only after exact
profile derivation and two isolated real-frame canaries. Failed candidates
remain staged while the previous working payload stays current.
An older supported first-run payload may use its pinned bootstrap mirror.
Options:
--source NAME Download source. Normal updates use apk-pure.
--version VERSION Request an exact provider version.
--launch Launch the current supported payload after updating.
--no-launch Never launch from this updater invocation.
--skip-build Reuse the existing Release binary for the canary.
--scheduled Run only when updates.automatic is true in YAML.
--startup-preflight Validate a runnable local payload without checking for
remote updates; provision one when none is available.
-h, --help Show this help.
EOF
}
Log() {
printf '[auto-update] %s\n' "$*" >&2
if [[ -n "${UPDATER_LOG}" ]]; then
printf '[auto-update] %s\n' "$*" >>"${UPDATER_LOG}" 2>/dev/null || true
fi
}
Progress() {
local -r descriptor="${MOCKTAIL_UPDATE_PROGRESS_FD:-}"
[[ "${descriptor}" =~ ^[0-9]{1,3}$ ]] || return 0
(( 10#${descriptor} >= 3 && 10#${descriptor} <= 255 )) || return 0
[[ -n "$*" ]] || return 0
printf 'P%s' "$*" >&"${descriptor}" 2>/dev/null || true
}
Die() {
Log "error: $*"
exit 1
}
ParseArguments() {
while (( $# > 0 )); do
case "$1" in
--source)
(( $# >= 2 )) || Die "--source requires a name"
SOURCE="$2"
shift 2
;;
--version)
(( $# >= 2 )) || Die "--version requires a value"
VERSION="$2"
if [[ "${VERSION}" =~ ^[0-9]+$ ]]; then
EXPECTED_PAYLOAD_VERSION_CODE="${VERSION}"
fi
shift 2
;;
--launch)
LAUNCH=true
shift
;;
--no-launch)
LAUNCH=false
shift
;;
--skip-build)
SKIP_BUILD=true
shift
;;
--scheduled)
SCHEDULED=true
shift
;;
--startup-preflight)
STARTUP_PREFLIGHT=true
shift
;;
-h|--help)
Usage
exit 0
;;
*)
Die "unknown option: $1"
;;
esac
done
}
LoadYamlDefaults() {
UPDATE_CONFIG_JSON="$(python3 "${SCRIPT_DIR}/read_update_config.py" "${CONFIG_FILE}")"
[[ -n "${SOURCE}" ]] || SOURCE="$(jq -r '.source // "apk-pure"' <<<"${UPDATE_CONFIG_JSON}")"
if [[ "${LAUNCH}" != true &&
"$(jq -r '.launch_after_update // false' <<<"${UPDATE_CONFIG_JSON}")" == true ]]; then
LAUNCH=true
fi
}
Cleanup() {
if [[ -n "${CANARY_ROOT}" ]]; then
rm -rf -- "${CANARY_ROOT}"
fi
if [[ -n "${PROBATION_ROOT}" ]]; then
rm -rf -- "${PROBATION_ROOT}"
fi
CleanupDownloadedBundle
if [[ -n "${UPDATER_LOG}" ]]; then
rm -f -- "${UPDATER_LOG}"
UPDATER_LOG=""
fi
}
CollectUpdaterSupportBundle() {
local -r exit_code="$1"
[[ "${MOCKTAIL_DISABLE_SUPPORT_BUNDLE:-0}" != 1 ]] || return 0
[[ "${SUPPORT_BUNDLE_COLLECTED}" != true ]] || return 0
local -r collector="${MOCKTAIL_SUPPORT_BUNDLE_SCRIPT:-${SCRIPT_DIR}/collect_support_bundle.sh}"
[[ "${collector}" == /* && -f "${collector}" &&
! -L "${collector}" && -x "${collector}" ]] || return 0
local -a arguments=(
--context updater
--reason "${SUPPORT_FAILURE_REASON}"
--exit-code "${exit_code}"
--output-dir "${STATE_ROOT}/support"
)
if [[ -n "${SUPPORT_CANDIDATE_METADATA}" &&
-f "${SUPPORT_CANDIDATE_METADATA}" &&
! -L "${SUPPORT_CANDIDATE_METADATA}" ]]; then
arguments+=(--payload-metadata "${SUPPORT_CANDIDATE_METADATA}")
fi
local source_log="${SUPPORT_SOURCE_LOG}"
[[ -n "${source_log}" ]] || source_log="${UPDATER_LOG}"
if [[ -n "${source_log}" && -f "${source_log}" &&
! -L "${source_log}" ]]; then
arguments+=(--log "${source_log}")
fi
local archive
archive="$("${collector}" "${arguments[@]}" 2>/dev/null || true)"
if [[ -n "${archive}" ]]; then
SUPPORT_BUNDLE_COLLECTED=true
local marker="${MOCKTAIL_UPDATE_SUPPORT_MARKER:-}"
if [[ "${marker}" == "${STATE_ROOT}"/.update-support.*.marker &&
"${marker##*/}" =~ ^\.update-support\.[0-9]+\.marker$ ]]; then
mkdir -m 0700 -- "${marker}" 2>/dev/null || true
fi
fi
}
OnExit() {
local -r exit_code="$1"
trap - EXIT
if (( exit_code != 0 )); then
CollectUpdaterSupportBundle "${exit_code}"
fi
Cleanup
exit "${exit_code}"
}
CleanupDownloadedBundle() {
[[ -n "${DOWNLOADED_BUNDLE_DIR}" ]] || return 0
local canonical_root canonical_bundle bundle_name
canonical_root="$(cd -- "${DOWNLOAD_STAGING_ROOT}" 2>/dev/null && pwd -P)" ||
return 0
canonical_bundle="$(cd -- "${DOWNLOADED_BUNDLE_DIR}" 2>/dev/null && pwd -P)" ||
return 0
bundle_name="${canonical_bundle##*/}"
if [[ "${canonical_bundle%/*}" != "${canonical_root}" ||
! "${bundle_name}" =~ ^com\.roblox\.client-[0-9]+-[0-9a-f]{12}-[0-9a-f]{12}$ ]]; then
Log "refusing to remove provider bundle outside the managed staging root"
return 0
fi
chmod -R u+w -- "${canonical_bundle}" 2>/dev/null || true
rm -rf -- "${canonical_bundle}"
DOWNLOADED_BUNDLE_DIR=""
}
AcquireUpdaterLock() {
SUPPORT_FAILURE_REASON=updater-lock-failed
exec 8>"${DATA_ROOT}/.auto-update.lock"
if flock -n 8; then
return 0
fi
if [[ "${STARTUP_PREFLIGHT}" != true ]]; then
Die "another automatic Roblox update is already running"
fi
Progress "Waiting for Roblox update..."
Log "another Roblox update is already running; waiting for its result"
flock 8 || Die "could not wait for the running Roblox update"
Log "the running Roblox update finished; validating its result"
}
ReleaseUpdaterLock() {
flock -u 8 2>/dev/null || true
exec 8>&-
}
VerifiedCurrentPayloadId() {
local payload_id
payload_id="$("${PAYLOAD_STORE_SCRIPT}" --root "${DATA_ROOT}" \
--compatibility "${COMPATIBILITY_PATH}" verify-current)" || return $?
[[ "${payload_id}" == none ||
"${payload_id}" =~ ^[0-9]+-[0-9a-f]{40}$ ]] || return 1
printf '%s\n' "${payload_id}"
}
CurrentPayloadId() {
local payload_id
payload_id="$(VerifiedCurrentPayloadId)" ||
Die "current payload or approval generation failed verification"
printf '%s\n' "${payload_id}"
}
LocalVersionCode() {
if [[ -n "${MOCKTAIL_UPDATE_LOCAL_VERSION+x}" ]]; then
[[ "${MOCKTAIL_UPDATE_LOCAL_VERSION}" =~ ^[0-9]+$ ]] || return 1
printf '%s\n' "${MOCKTAIL_UPDATE_LOCAL_VERSION}"
return 0
fi
local current_payload_id
current_payload_id="$(CurrentPayloadId)"
if [[ "${current_payload_id}" != none ]]; then
printf '%s\n' "${current_payload_id%%-*}"
return 0
fi
if [[ -f "${PROJECT_ROOT}/rbx_bin/libroblox.so" &&
! -L "${PROJECT_ROOT}/rbx_bin/libroblox.so" &&
-f "${PROJECT_ROOT}/rbx_bin/sober_apk/base.apk" ]] &&
command -v aapt >/dev/null 2>&1; then
aapt dump badging "${PROJECT_ROOT}/rbx_bin/sober_apk/base.apk" 2>/dev/null |
sed -n "s/^package:.* versionCode='\([0-9][0-9]*\)'.*/\1/p" |
head -n 1
fi
}
CheckLatestVersion() {
if [[ -n "${VERSION_CHECK_COMMAND}" ]]; then
"${VERSION_CHECK_COMMAND}" "${SOURCE}"
return
fi
local -r provider="${PROVIDER_DIR}/direct_apkpure.sh"
[[ -x "${provider}" ]] || return 1
local -a arguments=(--check --package com.roblox.client --source "${SOURCE}")
local -r timeout_seconds="${MOCKTAIL_UPDATE_CHECK_TIMEOUT_S:-5}"
[[ "${timeout_seconds}" =~ ^[1-9][0-9]?$ ]] && (( timeout_seconds <= 30 )) ||
Die "MOCKTAIL_UPDATE_CHECK_TIMEOUT_S must be between 1 and 30"
timeout --signal=TERM --kill-after=1 "${timeout_seconds}s" \
"${provider}" "${arguments[@]}"
}
FindStagedVersion() {
local -r version_code="$1"
local metadata selected_payload_id=""
for metadata in "${DATA_ROOT}"/payloads/*/roblox_payload.json; do
[[ -f "${metadata}" ]] || continue
local payload_dir="${metadata%/roblox_payload.json}"
if [[ ! -L "${metadata}" &&
-f "${payload_dir}/libroblox.so" &&
! -L "${payload_dir}/libroblox.so" &&
-d "${payload_dir}/assets/content" &&
! -L "${payload_dir}/assets/content" &&
"$(jq -r '.version_code // empty' "${metadata}")" == "${version_code}" ]]; then
if [[ -n "${selected_payload_id}" &&
"${selected_payload_id}" != "${payload_dir##*/}" ]]; then
STAGED_PAYLOAD_ID=""
Log "multiple staged Build IDs share versionCode ${version_code}; downloading provider latest to resolve it"
return 2
fi
selected_payload_id="${payload_dir##*/}"
fi
done
[[ -n "${selected_payload_id}" ]] || return 1
STAGED_PAYLOAD_ID="${selected_payload_id}"
}
FindUniqueSupportedStagedVersion() {
local -r version_code="$1"
local metadata payload_dir payload_id build_id
local selected_payload_id=""
for metadata in "${DATA_ROOT}"/payloads/*/roblox_payload.json; do
[[ -f "${metadata}" && ! -L "${metadata}" ]] || continue
payload_dir="${metadata%/roblox_payload.json}"
[[ -d "${payload_dir}" && ! -L "${payload_dir}" &&
-f "${payload_dir}/libroblox.so" &&
! -L "${payload_dir}/libroblox.so" &&
-d "${payload_dir}/assets/content" &&
! -L "${payload_dir}/assets/content" ]] || continue
payload_id="${payload_dir##*/}"
build_id="$(jq -er '.elf_build_id | select(type == "string") |
ascii_downcase | select(test("^[0-9a-f]{40}$"))' \
"${metadata}" 2>/dev/null || true)"
[[ -n "${build_id}" &&
"${payload_id}" == "${version_code}-${build_id}" &&
"$(jq -r '.version_code // empty' "${metadata}")" == \
"${version_code}" ]] || continue
IsSupportedCandidate "${metadata}" || continue
if [[ -n "${selected_payload_id}" &&
"${selected_payload_id}" != "${payload_id}" ]]; then
Log "multiple exact-supported staged payloads share versionCode ${version_code}; refusing an ambiguous first-run choice"
STAGED_PAYLOAD_ID=""
return 2
fi
selected_payload_id="${payload_id}"
done
[[ -n "${selected_payload_id}" ]] || return 1
STAGED_PAYLOAD_ID="${selected_payload_id}"
}
FindNewestSupportedStagedCandidate() {
local -r minimum_version_code="$1"
local -r maximum_version_code="$2"
[[ "${minimum_version_code}" =~ ^[0-9]+$ &&
"${maximum_version_code}" =~ ^[0-9]+$ ]] || return 1
local metadata payload_dir payload_id version_code build_id
local best_payload_id="" best_version_code=0 ambiguous=false
for metadata in "${DATA_ROOT}"/payloads/*/roblox_payload.json; do
[[ -f "${metadata}" && ! -L "${metadata}" ]] || continue
payload_dir="${metadata%/roblox_payload.json}"
[[ -d "${payload_dir}" && ! -L "${payload_dir}" &&
-f "${payload_dir}/libroblox.so" &&
! -L "${payload_dir}/libroblox.so" &&
-d "${payload_dir}/assets/content" &&
! -L "${payload_dir}/assets/content" ]] || continue
payload_id="${payload_dir##*/}"
version_code="$(jq -er '.version_code |
select(type == "number" and . > 0 and floor == .)' \
"${metadata}" 2>/dev/null || true)"
build_id="$(jq -er '.elf_build_id | select(type == "string") |
ascii_downcase | select(test("^[0-9a-f]{40}$"))' \
"${metadata}" 2>/dev/null || true)"
[[ -n "${version_code}" && -n "${build_id}" &&
"${payload_id}" == "${version_code}-${build_id}" ]] || continue
(( 10#${version_code} > 10#${minimum_version_code} &&
10#${version_code} <= 10#${maximum_version_code} )) || continue
IsSupportedCandidate "${metadata}" || continue
if (( 10#${version_code} > 10#${best_version_code} )); then
best_version_code="${version_code}"
best_payload_id="${payload_id}"
ambiguous=false
elif (( 10#${version_code} == 10#${best_version_code} )) &&
[[ "${payload_id}" != "${best_payload_id}" ]]; then
ambiguous=true
fi
done
[[ -n "${best_payload_id}" ]] || return 1
if [[ "${ambiguous}" == true ]]; then
Log "multiple exact-supported staged payloads share versionCode ${best_version_code}; refusing an ambiguous automatic choice"
return 2
fi
STAGED_PAYLOAD_ID="${best_payload_id}"
}
LatestSupportedBootstrapProfile() {
local -r remote_version_code="${1:-}"
jq -er --arg remote_version_code "${remote_version_code}" '
[.profiles[]? |
select(($remote_version_code == "" or
.version_code <= ($remote_version_code | tonumber)) and
.status == "supported" and .default_allowed == true and
.allow_legacy_binary_patches == false and
(.abi // "x86_64") == "x86_64" and
(.version_name | type) == "string" and
(.version_name | length) > 0)] |
group_by(.version_code) |
map(select(length == 1) | .[0]) |
sort_by(.version_code) | last | select(type == "object") |
[.version_code, .version_name] | @tsv
' "${COMPATIBILITY_PATH}"
}
SelectSupportedBootstrapFallback() {
local -r remote_version_code="${1:-}"
local profile fallback_version_code fallback_version_name
profile="$(LatestSupportedBootstrapProfile "${remote_version_code}" || true)"
[[ -n "${profile}" ]] ||
Die "no supported Roblox compatibility profile is available for first-run bootstrap"
IFS=$'\t' read -r fallback_version_code fallback_version_name <<<"${profile}"
[[ "${fallback_version_code}" =~ ^[0-9]+$ &&
-n "${fallback_version_name}" ]] ||
Die "supported first-run compatibility profile is invalid"
BOOTSTRAP_SELECTED_VERSION_CODE="${fallback_version_code}"
EXPECTED_PAYLOAD_VERSION_CODE="${fallback_version_code}"
local bootstrap_source=""
if [[ -e "${BOOTSTRAP_SOURCES_PATH}" ||
-L "${BOOTSTRAP_SOURCES_PATH}" ]]; then
[[ -f "${BOOTSTRAP_SOURCES_PATH}" &&
! -L "${BOOTSTRAP_SOURCES_PATH}" ]] ||
Die "bootstrap source manifest must be a regular file"
jq -e '.schema_version == 1 and (.sources | type == "array")' \
"${BOOTSTRAP_SOURCES_PATH}" >/dev/null ||
Die "bootstrap source manifest is invalid"
bootstrap_source="$(jq -er --argjson version_code "${fallback_version_code}" '
[.sources[]? |
select(.version_code == $version_code and
(.abi // "x86_64") == "x86_64" and
.provider == "uptodown")] |
if length == 1 then .[0].provider else empty end
' "${BOOTSTRAP_SOURCES_PATH}" 2>/dev/null || true)"
fi
if [[ -n "${bootstrap_source}" ]]; then
SOURCE="${bootstrap_source}"
fi
STAGED_PAYLOAD_ID=""
local staged_selection_status=0
FindUniqueSupportedStagedVersion "${fallback_version_code}" ||
staged_selection_status=$?
if (( staged_selection_status == 0 )); then
if [[ "${remote_version_code}" == "${fallback_version_code}" ]]; then
Log "reusing exact-supported first-run payload ${fallback_version_code}"
elif [[ -n "${remote_version_code}" ]]; then
Log "latest Roblox versionCode ${remote_version_code} is not supported yet; reusing supported first-run payload ${fallback_version_code}"
else
Log "latest-version API is unavailable; reusing supported first-run payload ${fallback_version_code}"
fi
return 0
fi
if (( staged_selection_status == 2 )); then
Die "supported first-run payload ${fallback_version_code} is ambiguous"
fi
VERSION="${fallback_version_name}"
if [[ "${remote_version_code}" == "${fallback_version_code}" ]]; then
Log "downloading exact-supported first-run payload ${fallback_version_name} (${fallback_version_code}) from ${SOURCE}"
elif [[ -n "${remote_version_code}" ]]; then
Log "latest Roblox versionCode ${remote_version_code} is not supported yet; downloading supported first-run payload ${fallback_version_name} (${fallback_version_code}) from ${SOURCE}"
else
Log "latest-version API is unavailable; downloading supported first-run payload ${fallback_version_name} (${fallback_version_code}) from ${SOURCE}"
fi
}
StageSupportedBootstrapFallback() {
local ceiling="${1:-}"
local selection status=1 selected_version
while selection="$(LatestSupportedBootstrapProfile "${ceiling}" 2>/dev/null)" &&
[[ -n "${selection}" ]]; do
SelectSupportedBootstrapFallback "${ceiling}"
selected_version="${BOOTSTRAP_SELECTED_VERSION_CODE}"
status=0
StagePlannedPayload || status=$?
(( status != 0 )) || return 0
Log "supported bootstrap ${selected_version} acquisition failed; trying the next lower exact-supported profile"
(( 10#${selected_version} > 1 )) || return "${status}"
ceiling="$((10#${selected_version} - 1))"
done
return "${status}"
}
SkipDownloadWhenCurrent() {
local refresh_status=0
RefreshApprovedCurrentForRuntime || refresh_status=$?
if (( refresh_status >= 2 )); then
Die "current payload is not runnable by this Mocktail runtime and rollback failed"
fi
[[ -z "${VERSION}" ]] || return 1
local local_version remote_json remote_version
local_version="$(LocalVersionCode || true)"
if ! remote_json="$(CheckLatestVersion)"; then
if [[ "${local_version}" =~ ^[0-9]+$ ]]; then
Log "latest-version API check failed; preserving the runnable local payload"
return 0
fi
Log "latest-version API check failed and no runnable local payload exists; using the compatibility bootstrap profile"
SelectSupportedBootstrapFallback
return 1
fi
remote_version="$(jq -r '.version_code // .latest_version_code // empty' <<<"${remote_json}")"
if [[ ! "${remote_version}" =~ ^[0-9]+$ ]]; then
if [[ "${local_version}" =~ ^[0-9]+$ ]]; then
Log "latest-version API returned invalid metadata; preserving the runnable local payload"
return 0
fi
Log "provider returned invalid version metadata and no runnable local payload exists; using the compatibility bootstrap profile"
SelectSupportedBootstrapFallback
return 1
fi
EXPECTED_PAYLOAD_VERSION_CODE="${remote_version}"
if [[ "${remote_version}" == "${local_version}" ]]; then
Log "Roblox versionCode ${local_version} is current; download skipped"
return 0
fi
if [[ "${local_version}" =~ ^[0-9]+$ ]] &&
(( 10#${remote_version} < 10#${local_version} )); then
Log "latest-version API regressed from local versionCode ${local_version} to ${remote_version}; refusing a downgrade"
return 0
fi
if FindStagedVersion "${remote_version}"; then
Log "reusing staged latest Roblox versionCode ${remote_version} for probation"
return 1
fi
if [[ ! "${local_version}" =~ ^[0-9]+$ ]]; then
Log "no runnable local Roblox payload; downloading latest versionCode ${remote_version} before fallback"
return 1
fi
Log "Roblox update available: versionCode ${local_version} -> ${remote_version}"
return 1
}
SkipRemoteUpdateForStartup() {
local refresh_status=0
RefreshApprovedCurrentForRuntime || refresh_status=$?
if (( refresh_status >= 2 )); then
Die "current payload is not runnable by this Mocktail runtime and rollback failed"
fi
local current_payload_id
current_payload_id="$(VerifiedCurrentPayloadId 2>/dev/null || true)"
if [[ "${current_payload_id}" =~ ^[0-9]+-[0-9a-f]{40}$ ]]; then
Log "startup preflight accepted runnable local payload ${current_payload_id}; remote update deferred to the background timer"
return 0
fi
return 1
}
IsSupportedCandidate() {
local -r metadata="$1"
local build_id version_code
build_id="$(jq -r '.elf_build_id | ascii_downcase' "${metadata}")"
version_code="$(jq -r '.version_code' "${metadata}")"
jq -e --arg build_id "${build_id}" --argjson version_code "${version_code}" '
any(.profiles[]?;
(.elf_build_id | ascii_downcase) == $build_id and
.version_code == $version_code and
.status == "supported" and .default_allowed == true and
.allow_legacy_binary_patches == false)
' "${COMPATIBILITY_PATH}" >/dev/null
}
BuildRuntime() {
SUPPORT_FAILURE_REASON=updater-build-failed
[[ -z "${PACKAGED_CANARY_BINARY}" ]] || return 0
[[ "${SKIP_BUILD}" == false ]] || return 0
Log "building canary runtime"
make -C "${PROJECT_ROOT}" --no-print-directory build \
BUILD_TYPE=Release JOBS="$(nproc)"
}
ResolveCanaryRuntime() {
local default_canary_binary="${PROJECT_ROOT}/build/mocktail"
[[ -z "${PACKAGED_CANARY_BINARY}" ]] ||
default_canary_binary="${PACKAGED_CANARY_BINARY}"
CANARY_BINARY="${MOCKTAIL_UPDATE_CANARY_BIN:-${default_canary_binary}}"
[[ -f "${CANARY_BINARY}" && ! -L "${CANARY_BINARY}" &&
-x "${CANARY_BINARY}" ]] || {
Log "canary binary is not an executable regular file: ${CANARY_BINARY}"
return 1
}
RUNTIME_FINGERPRINT="$(sha256sum "${CANARY_BINARY}" | awk '{print $1}')"
RUNTIME_BUILD_ID="${MOCKTAIL_UPDATE_RUNTIME_BUILD_ID:-$(
LC_ALL=C readelf -n "${CANARY_BINARY}" 2>/dev/null |
sed -n 's/^[[:space:]]*Build ID: \([0-9A-Fa-f]\{40\}\)$/\1/p' |
head -n 1
)}"
RUNTIME_BUILD_ID="${RUNTIME_BUILD_ID,,}"
[[ "${RUNTIME_FINGERPRINT}" =~ ^[0-9a-f]{64}$ &&
"${RUNTIME_BUILD_ID}" =~ ^[0-9a-f]{40}$ ]]
}
ApprovalMatchesResolvedRuntime() {
local -r approval_file="$1"
[[ -f "${approval_file}" && ! -L "${approval_file}" &&
"${RUNTIME_FINGERPRINT}" =~ ^[0-9a-f]{64}$ &&
"${RUNTIME_BUILD_ID}" =~ ^[0-9a-f]{40}$ ]] || return 1
jq -e --arg runtime_build_id "${RUNTIME_BUILD_ID}" \
--arg runtime_sha256 "${RUNTIME_FINGERPRINT}" '
(.runtime_build_id | type == "string") and
(.runtime_build_id | ascii_downcase) == $runtime_build_id and
.runtime_sha256 == $runtime_sha256 and
.canary_runtime_sha256 == $runtime_sha256
' "${approval_file}" >/dev/null
}
ResolveDerivationReference() {
REFERENCE_LIBRARY=""
REFERENCE_PROFILE=""
local -r reference_build_id="${MOCKTAIL_UPDATE_REFERENCE_BUILD_ID:-1686400865ae0e408cd7bd67de7a439625c6fd13}"
local -r reference_profile="${MOCKTAIL_UPDATE_REFERENCE_PROFILE:-${DEFAULT_METADATA_ROOT}/roblox_host_abi_reference.json}"
local metadata payload_dir build_id
if [[ -f "${DATA_ROOT}/current.json" &&
! -L "${DATA_ROOT}/current.json" ]]; then
local current_payload_path current_profile_path
current_payload_path="$(jq -er '.payload_path |
select(test("^payloads/[0-9]+-[0-9a-f]{40}$"))' \
"${DATA_ROOT}/current.json" 2>/dev/null || true)"
current_profile_path="$(jq -er '.host_abi_profile_path // empty |
select(test("^host_abi_profiles/[0-9]+-[0-9a-f]{40}-[0-9a-f]{40}\\.json$"))' \
"${DATA_ROOT}/current.json" 2>/dev/null || true)"
if [[ -n "${current_payload_path}" ]]; then
payload_dir="${DATA_ROOT}/${current_payload_path}"
metadata="${payload_dir}/roblox_payload.json"
build_id="$(jq -er '.elf_build_id | ascii_downcase' \
"${metadata}" 2>/dev/null || true)"
if [[ "${build_id}" == "${reference_build_id}" &&
-f "${payload_dir}/libroblox.so" &&
! -L "${payload_dir}/libroblox.so" &&
-f "${reference_profile}" && ! -L "${reference_profile}" ]]; then
REFERENCE_LIBRARY="${payload_dir}/libroblox.so"
REFERENCE_PROFILE="${reference_profile}"
return 0
fi
if [[ -n "${current_profile_path}" &&
-f "${DATA_ROOT}/${current_profile_path}" &&
! -L "${DATA_ROOT}/${current_profile_path}" &&
-f "${payload_dir}/libroblox.so" &&
! -L "${payload_dir}/libroblox.so" ]]; then
REFERENCE_LIBRARY="${payload_dir}/libroblox.so"
REFERENCE_PROFILE="${DATA_ROOT}/${current_profile_path}"
return 0
fi
fi
fi
for metadata in "${DATA_ROOT}"/payloads/*/roblox_payload.json; do
[[ -f "${metadata}" && ! -L "${metadata}" ]] || continue
build_id="$(jq -er '.elf_build_id | ascii_downcase' \
"${metadata}" 2>/dev/null || true)"
[[ "${build_id}" == "${reference_build_id}" ]] || continue
payload_dir="${metadata%/roblox_payload.json}"
[[ -f "${payload_dir}/libroblox.so" &&
! -L "${payload_dir}/libroblox.so" &&
-f "${reference_profile}" && ! -L "${reference_profile}" ]] ||
continue
REFERENCE_LIBRARY="${payload_dir}/libroblox.so"
REFERENCE_PROFILE="${reference_profile}"
return 0
done
return 3
}
DeriveCandidateProfile() {
local -r payload_dir="$1"
local -r payload_id="$2"
SUPPORT_FAILURE_REASON=profile-derivation-failed
ResolveDerivationReference || return $?
[[ -f "${PROFILE_DERIVER}" && ! -L "${PROFILE_DERIVER}" ]] || {
Log "host-ABI profile deriver is unavailable: ${PROFILE_DERIVER}"
return 2
}
[[ -n "${PROBATION_ROOT}" ]] ||
PROBATION_ROOT="$(mktemp -d "${CACHE_ROOT}/.update-probation.${payload_id}.XXXXXX")"
CANDIDATE_PROFILE="${PROBATION_ROOT}/host_abi_profile.json"
CANDIDATE_COMPATIBILITY="${PROBATION_ROOT}/compatibility.json"
local -a derive_arguments=(
--reference-lib "${REFERENCE_LIBRARY}"
--candidate-lib "${payload_dir}/libroblox.so"
--payload-metadata "${payload_dir}/roblox_payload.json"
--output "${CANDIDATE_PROFILE}"
--compatibility-output "${CANDIDATE_COMPATIBILITY}"
)
[[ -n "${REFERENCE_PROFILE}" ]] || return 3
derive_arguments+=(--reference-profile "${REFERENCE_PROFILE}")
Progress "Checking Roblox..."
Log "deriving exact host-ABI profile for ${payload_id}"
if ! python3 "${PROFILE_DERIVER}" "${derive_arguments[@]}"; then
Log "candidate ${payload_id} profile derivation was not unique"
return 4
fi
[[ -f "${CANDIDATE_PROFILE}" && ! -L "${CANDIDATE_PROFILE}" &&
-f "${CANDIDATE_COMPATIBILITY}" &&
! -L "${CANDIDATE_COMPATIBILITY}" ]] || return 4
local -r payload_sha256="$(sha256sum \
"${payload_dir}/libroblox.so" | awk '{print $1}')"
jq -e --arg payload_id "${payload_id}" \
--arg payload_path "payloads/${payload_id}" \
--arg build_id "${payload_id#*-}" \
--arg payload_sha256 "${payload_sha256}" '
.schema_version == 1 and .payload_id == $payload_id and
.payload_path == $payload_path and
(.elf_build_id | ascii_downcase) == $build_id and
.payload_sha256 == $payload_sha256 and
(.profile | type) == "object"
' "${CANDIDATE_PROFILE}" >/dev/null || return 4
jq -e --arg build_id "${payload_id#*-}" \
--argjson version_code "${payload_id%%-*}" '
.schema_version == 1 and (.profiles | length) == 1 and
.profiles[0].version_code == $version_code and
(.profiles[0].elf_build_id | ascii_downcase) == $build_id and
.profiles[0].status == "experimental" and
.profiles[0].default_allowed == true and
.profiles[0].allow_legacy_binary_patches == false and
.profiles[0].allow_host_abi_bridges == true and
.profiles[0].allow_host_constructor_replay == true
' "${CANDIDATE_COMPATIBILITY}" >/dev/null || return 4
CANDIDATE_IS_PROBATION=true
}
RunCandidateCanary() {
SUPPORT_FAILURE_REASON=canary-failed
local -r payload_dir="$1"
local -r payload_id="$2"
local -r run_index="${3:-1}"
local -r candidate_fingerprint="${4:-}"
local -r timeout_seconds="${MOCKTAIL_UPDATE_CANARY_TIMEOUT_S:-150}"
[[ "${timeout_seconds}" =~ ^[1-9][0-9]{0,2}$ ]] &&
(( 10#${timeout_seconds} <= 600 )) ||
Die "MOCKTAIL_UPDATE_CANARY_TIMEOUT_S must be between 1 and 600"
[[ -n "${CANARY_BINARY}" && -n "${RUNTIME_FINGERPRINT}" ]] ||
ResolveCanaryRuntime || return 2
CANARY_ROOT="$(mktemp -d "${CACHE_ROOT}/.update-canary.${payload_id}.${run_index}.XXXXXX")"
local -r canary_log_root="${STATE_ROOT}/logs/update-canary/${payload_id}"
mkdir -p "${canary_log_root}"
local canary_log_dir
canary_log_dir="$(mktemp -d "${canary_log_root}/run.XXXXXX")"
mkdir -p "${CANARY_ROOT}/home" "${CANARY_ROOT}/data" \
"${CANARY_ROOT}/cache" "${CANARY_ROOT}/state" \
"${CANARY_ROOT}/config"
local -a canary_environment=()
local environment_entry environment_name
while IFS= read -r -d '' environment_entry; do
environment_name="${environment_entry%%=*}"
case "${environment_name}" in
DISPLAY|WAYLAND_DISPLAY|XDG_RUNTIME_DIR|DBUS_SESSION_BUS_ADDRESS|\
PULSE_SERVER|PIPEWIRE_REMOTE|LANG|LANGUAGE|LC_*|TZ|USER|LOGNAME)
canary_environment+=("${environment_entry}")
;;
VK_DRIVER_FILES|VK_ICD_FILENAMES|MESA_LOADER_DRIVER_OVERRIDE|DRI_PRIME|\
__NV_PRIME_RENDER_OFFLOAD|__VK_LAYER_NV_optimus|\
__GLX_VENDOR_LIBRARY_NAME)
canary_environment+=("${environment_entry}")
;;
FAKE_*|EXPECTED_*|REAL_*|CANARY_MARKER|LIVE_CONFIG_ROOT)
[[ "${MOCKTAIL_UPDATE_TEST_ENV_PASSTHROUGH:-0}" == 1 ]] &&
canary_environment+=("${environment_entry}")
;;
esac
done < <(env -0)
local canary_xauthority=""
if [[ -n "${XAUTHORITY:-}" && -f "${XAUTHORITY}" &&
! -L "${XAUTHORITY}" ]]; then
canary_xauthority="${CANARY_ROOT}/config/Xauthority"
cp -- "${XAUTHORITY}" "${canary_xauthority}"
chmod 0600 -- "${canary_xauthority}"
canary_environment+=("XAUTHORITY=${canary_xauthority}")
fi
local canary_path=/usr/bin:/bin
if [[ "${DEFAULT_METADATA_ROOT}" == "${PROJECT_ROOT}/metadata" ]]; then
local packaged_android_tools="${PROJECT_ROOT}/runtime/android-tools/bin"
local packaged_runtime_bin="${PROJECT_ROOT}/runtime/bin"
if [[ -d "${packaged_android_tools}" &&
! -L "${packaged_android_tools}" ]]; then
canary_path="${packaged_android_tools}:${canary_path}"
fi
if [[ -d "${packaged_runtime_bin}" &&
! -L "${packaged_runtime_bin}" ]]; then
canary_path="${packaged_runtime_bin}:${canary_path}"
fi
fi
local compatibility_manifest="${COMPATIBILITY_PATH}"
local -a probation_environment=()
local -a smoke_arguments=(C)
if [[ "${CANDIDATE_IS_PROBATION}" == true ]]; then
compatibility_manifest="${CANDIDATE_COMPATIBILITY}"
probation_environment+=(
MOCKTAIL_HOST_ABI_PROFILE_FILE="${CANDIDATE_PROFILE}"
MOCKTAIL_HOST_ABI_CANARY=1
MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI=1
)
smoke_arguments+=(--allow-unverified-build)
fi
Progress "Testing Roblox..."
Log "running isolated real Vulkan Tier C canary ${run_index} for ${payload_id}"
local canary_status
set +e
env -i "${canary_environment[@]}" \
HOME="${CANARY_ROOT}/home" \
XDG_DATA_HOME="${CANARY_ROOT}/data" \
XDG_CACHE_HOME="${CANARY_ROOT}/cache" \
XDG_STATE_HOME="${CANARY_ROOT}/state" \
XDG_CONFIG_HOME="${CANARY_ROOT}/config" \
PATH="${canary_path}" \
"${probation_environment[@]}" \
ROBLOX_LIB_PATH="${payload_dir}/libroblox.so" \
MOCKTAIL_ASSET_ROOT="${payload_dir}/assets" \
MOCKTAIL_ASSET_PATH="${payload_dir}/assets/content" \
MOCKTAIL_DATA_ROOT="${CANARY_ROOT}/data" \
MOCKTAIL_CACHE_ROOT="${CANARY_ROOT}/cache" \
MOCKTAIL_STATE_ROOT="${CANARY_ROOT}/state" \
MOCKTAIL_CONFIG_ROOT="${CANARY_ROOT}/config" \
MOCKTAIL_LOG_DIR="${canary_log_dir}" \
MOCKTAIL_BIN="${CANARY_BINARY}" \
MOCKTAIL_COMPATIBILITY_MANIFEST="${compatibility_manifest}" \
MOCKTAIL_SKIP_LIBROBLOX_CTORS=0 \
MOCKTAIL_HOST_JNI_SINGLETON_SEED=0 \
MOCKTAIL_SKIP_UPDATE_CHECK=1 \
MOCKTAIL_ISOLATED_CANARY=1 \
MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP=1 \
MOCKTAIL_IGNORE_WINDOW_CLOSE=1 \
MOCKTAIL_DISABLE_SUPPORT_BUNDLE=1 \
MOCKTAIL_AUTO_EXIT_AFTER_PRESENT_MS=5000 \
timeout --signal=TERM --kill-after=5s "${timeout_seconds}s" \
"${SMOKE_SCRIPT}" "${smoke_arguments[@]}" 8>&-
canary_status=$?
set -e
CANARY_LOG="$(find "${canary_log_dir}" -maxdepth 1 -type f \
-name 'tierC_*.log' -printf '%f\n' 2>/dev/null | sort | tail -n 1)"
[[ -z "${CANARY_LOG}" ]] || CANARY_LOG="${canary_log_dir}/${CANARY_LOG}"
rm -rf -- "${CANARY_ROOT}"
CANARY_ROOT=""
if (( canary_status != 0 )); then
Log "candidate ${payload_id} failed canary with status ${canary_status}; current payload was not changed"
SUPPORT_CANDIDATE_METADATA="${payload_dir}/roblox_payload.json"
SUPPORT_SOURCE_LOG="${CANARY_LOG}"
SUPPORT_FAILURE_REASON=canary-failed
CollectUpdaterSupportBundle "${canary_status}"
return "${canary_status}"
fi
SUPPORT_SOURCE_LOG="${CANARY_LOG}"
if [[ "${CANDIDATE_IS_PROBATION}" == true ]]; then
if [[ -z "${CANARY_LOG}" || ! -f "${CANARY_LOG}" ||
-L "${CANARY_LOG}" || ! -s "${CANARY_LOG}" ]]; then
SUPPORT_FAILURE_REASON=canary-readiness-log-missing
Log "candidate ${payload_id} returned success without a readiness log"
CollectUpdaterSupportBundle 5
return 5
fi
local -r log_sha256="$(sha256sum \
"${CANARY_LOG}" | awk '{print $1}')"
local -r profile_sha256="$(sha256sum \
"${CANDIDATE_PROFILE}" | awk '{print $1}')"
local -r compatibility_sha256="$(sha256sum \
"${CANDIDATE_COMPATIBILITY}" | awk '{print $1}')"
local attestation_path
attestation_path="${PROBATION_ROOT}/canary-${run_index}.json"
jq -n --argjson run "${run_index}" \
--arg run_id "${payload_id}-${run_index}-$(date +%s)-$$" \
--arg payload_id "${payload_id}" \
--arg payload_fingerprint "${candidate_fingerprint}" \
--arg profile_sha256 "${profile_sha256}" \
--arg compatibility_sha256 "${compatibility_sha256}" \
--arg runtime_sha256 "${RUNTIME_FINGERPRINT}" \
--arg runtime_build_id "${RUNTIME_BUILD_ID}" \
--arg readiness_log_sha256 "${log_sha256}" \
'{schema_version:1,status:"passed",canary_tier:"C",run:$run,
run_id:$run_id,payload_id:$payload_id,
payload_fingerprint:$payload_fingerprint,
profile_sha256:$profile_sha256,
compatibility_manifest_sha256:$compatibility_sha256,
runtime_sha256:$runtime_sha256,
runtime_build_id:$runtime_build_id,
readiness_log_sha256:$readiness_log_sha256}' > "${attestation_path}"
chmod 0400 -- "${attestation_path}"
CANARY_ATTESTATIONS+=("${attestation_path}")
fi
CANARY_LOG=""
}
RunCandidateCanaries() {
local -r payload_dir="$1"
local -r payload_id="$2"
local -r candidate_fingerprint="$3"
CANARY_ATTESTATIONS=()
local canary_runs=1 run_index
[[ "${CANDIDATE_IS_PROBATION}" == false ]] || canary_runs=2
for ((run_index = 1; run_index <= canary_runs; ++run_index)); do
RunCandidateCanary "${payload_dir}" "${payload_id}" "${run_index}" \
"${candidate_fingerprint}" || return $?
done
}
LaunchCurrent() {
SUPPORT_FAILURE_REASON=updater-launch-failed
local current_payload_id
current_payload_id="$(CurrentPayloadId)"
[[ "${current_payload_id}" != none ]] || Die "no current payload is available"
local payload_path
payload_path="$(jq -er '.payload_path | select(type == "string")' \
"${DATA_ROOT}/current.json")" || Die "current payload manifest is missing"
[[ "${payload_path}" =~ ^payloads/[0-9]+-[0-9a-f]{40}$ ]] ||
Die "current payload path is invalid"
local -r payload_dir="${DATA_ROOT}/${payload_path}"
local -a approval_environment=()
local approval_path profile_path compatibility_path
approval_path="$(jq -r '.approval_path // empty' \
"${DATA_ROOT}/current.json")"
profile_path="$(jq -r '.host_abi_profile_path // empty' \
"${DATA_ROOT}/current.json")"
compatibility_path="$(jq -r '.compatibility_manifest_path // empty' \
"${DATA_ROOT}/current.json")"
if [[ -n "${approval_path}${profile_path}${compatibility_path}" ]]; then
local approval_generation=""
if [[ "${approval_path}" =~ ^approvals/${current_payload_id}-([0-9a-f]{40})\.json$ ]]; then
approval_generation="${BASH_REMATCH[1]}"
fi
[[ -n "${approval_generation}" &&
"${profile_path}" == \
"host_abi_profiles/${current_payload_id}-${approval_generation}.json" &&
"${compatibility_path}" == \
"compatibility_profiles/${current_payload_id}-${approval_generation}.json" ]] ||
Die "current probation approval paths are invalid"
local approval_file profile_file compatibility_file
approval_file="${DATA_ROOT}/${approval_path}"
profile_file="${DATA_ROOT}/${profile_path}"
compatibility_file="${DATA_ROOT}/${compatibility_path}"
local approval_artifact
for approval_artifact in "${approval_file}" "${profile_file}" \
"${compatibility_file}"; do
[[ -f "${approval_artifact}" && ! -L "${approval_artifact}" ]] ||
Die "current probation approval artifact is invalid"
done
ResolveCanaryRuntime || Die "cannot identify the launch runtime"
ApprovalMatchesResolvedRuntime "${approval_file}" ||
Die "current probation approval belongs to a different Mocktail runtime"
approval_environment+=(
MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT="${approval_file}"
MOCKTAIL_HOST_ABI_PROFILE_FILE="${profile_file}"
MOCKTAIL_COMPATIBILITY_MANIFEST="${compatibility_file}"
)
fi
Log "launching current payload from ${payload_dir}"
Cleanup
ReleaseUpdaterLock
exec env -u MOCKTAIL_HOST_ABI_CANARY \
-u MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI \
-u MOCKTAIL_HOST_ABI_PROFILE_FILE \
-u MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT \
-u MOCKTAIL_COMPATIBILITY_MANIFEST \
-u MOCKTAIL_HOST_ALLOCATOR_BRIDGES \
-u MOCKTAIL_PATCH_HOSTILE_CANARY \
-u MOCKTAIL_SKIP_LIBROBLOX_CTORS \
-u MOCKTAIL_HOST_JNI_SINGLETON_SEED \
-u MOCKTAIL_ROBLOX_COOKIES \
-u BASH_ENV -u ENV -u LD_PRELOAD -u LD_AUDIT \
-u RIPGREP_CONFIG_PATH \
"${approval_environment[@]}" \
ROBLOX_LIB_PATH="${payload_dir}/libroblox.so" \
MOCKTAIL_ASSET_ROOT="${payload_dir}/assets" \
MOCKTAIL_ASSET_PATH="${payload_dir}/assets/content" \
MOCKTAIL_SKIP_UPDATE_CHECK=1 \
MOCKTAIL_SKIP_LIBROBLOX_CTORS=0 \
MOCKTAIL_HOST_JNI_SINGLETON_SEED=0 \
MOCKTAIL_AUTO_EXIT_AFTER_PRESENT_MS=0 \
"${SMOKE_SCRIPT}" C
}
ResetCandidateProbation() {
if [[ -n "${PROBATION_ROOT}" ]]; then
rm -rf -- "${PROBATION_ROOT}"
fi
PROBATION_ROOT=""
CANDIDATE_PROFILE=""
CANDIDATE_COMPATIBILITY=""
CANDIDATE_IS_PROBATION=false
CANARY_ATTESTATIONS=()
}
BuildCandidateRejectionContext() {
local -r payload_id="$1"
local -r payload_fingerprint="$2"
CANDIDATE_REJECTION_PROFILE_SHA256="$(sha256sum \
"${CANDIDATE_PROFILE}" | awk '{print $1}')"
CANDIDATE_REJECTION_COMPATIBILITY_SHA256="$(sha256sum \
"${CANDIDATE_COMPATIBILITY}" | awk '{print $1}')"
CANDIDATE_REJECTION_REFERENCE_SHA256="$(jq -Sc '.reference' \
"${CANDIDATE_PROFILE}" | sha256sum | awk '{print $1}')"
CANDIDATE_REJECTION_DERIVER_SHA256="$(sha256sum \
"${PROFILE_DERIVER}" | awk '{print $1}')"
CANDIDATE_REJECTION_SCHEMA_VERSION="$(jq -er '.schema_version |
select(type == "number" and floor == . and . >= 1)' \
"${CANDIDATE_PROFILE}")"
CANDIDATE_REJECTION_KEY="$(printf '%s\n' \
"payload_id=${payload_id}" \
"payload_fingerprint=${payload_fingerprint}" \
"profile=${CANDIDATE_REJECTION_PROFILE_SHA256}" \
"compatibility=${CANDIDATE_REJECTION_COMPATIBILITY_SHA256}" \
"runtime_build_id=${RUNTIME_BUILD_ID}" \
"runtime=${RUNTIME_FINGERPRINT}" \
"reference=${CANDIDATE_REJECTION_REFERENCE_SHA256}" \
"deriver=${CANDIDATE_REJECTION_DERIVER_SHA256}" \
"schema=${CANDIDATE_REJECTION_SCHEMA_VERSION}" |
sha256sum | awk '{print $1}')"
[[ "${CANDIDATE_REJECTION_KEY}" =~ ^[0-9a-f]{64}$ ]]
}
CandidateRejectionMemoPath() {
printf '%s/rejected_candidates/%s.json\n' "${STATE_ROOT}" "$1"
}
CandidateRejectionIsActive() {
local -r payload_id="$1"
[[ "${MOCKTAIL_UPDATE_RETRY_REJECTED:-0}" != 1 ]] || return 1
local -r memo_path="$(CandidateRejectionMemoPath "${payload_id}")"
[[ -f "${memo_path}" && ! -L "${memo_path}" ]] || return 1
local -r now="$(date +%s)"
local rejected_at
rejected_at="$(jq -er --arg payload_id "${payload_id}" \
--arg key "${CANDIDATE_REJECTION_KEY}" '
select(.schema_version == 1 and .payload_id == $payload_id and
.rejection_key == $key) |
.rejected_at_epoch |
select(type == "number" and floor == . and . >= 0)
' "${memo_path}" 2>/dev/null)" || return 1
local ttl_seconds="${MOCKTAIL_UPDATE_REJECTION_BACKOFF_S:-21600}"
[[ "${ttl_seconds}" =~ ^[1-9][0-9]*$ ]] || return 1
(( 10#${ttl_seconds} <= 604800 &&
10#${rejected_at} <= 10#${now} &&
10#${now} - 10#${rejected_at} < 10#${ttl_seconds} ))
}
RecordCandidateRejection() {
local -r payload_id="$1"
local -r canary_status="$2"
local -r memo_path="$(CandidateRejectionMemoPath "${payload_id}")"
mkdir -p -- "${memo_path%/*}"
local temporary
temporary="$(mktemp "${memo_path%/*}/.rejection.XXXXXX")"
if ! jq -n \
--arg payload_id "${payload_id}" \
--arg rejection_key "${CANDIDATE_REJECTION_KEY}" \
--arg profile_sha256 "${CANDIDATE_REJECTION_PROFILE_SHA256}" \
--arg compatibility_sha256 \
"${CANDIDATE_REJECTION_COMPATIBILITY_SHA256}" \
--arg runtime_build_id "${RUNTIME_BUILD_ID}" \
--arg runtime_sha256 "${RUNTIME_FINGERPRINT}" \
--arg reference_sha256 "${CANDIDATE_REJECTION_REFERENCE_SHA256}" \
--arg deriver_sha256 "${CANDIDATE_REJECTION_DERIVER_SHA256}" \
--argjson analyzer_schema_version \
"${CANDIDATE_REJECTION_SCHEMA_VERSION}" \
--argjson canary_status "${canary_status}" \
--argjson rejected_at_epoch "$(date +%s)" '
{schema_version:1,payload_id:$payload_id,
rejection_key:$rejection_key,profile_sha256:$profile_sha256,
compatibility_manifest_sha256:$compatibility_sha256,
runtime_build_id:$runtime_build_id,runtime_sha256:$runtime_sha256,
reference_profile_sha256:$reference_sha256,
deriver_sha256:$deriver_sha256,
analyzer_schema_version:$analyzer_schema_version,
canary_status:$canary_status,rejected_at_epoch:$rejected_at_epoch}
' > "${temporary}" ||
! chmod 0600 -- "${temporary}" ||
! mv -f -- "${temporary}" "${memo_path}"; then
rm -f -- "${temporary}"
Log "warning: failed to persist candidate rejection backoff" || true
fi
}
ClearCandidateRejection() {
local -r payload_id="$1"
rm -f -- "$(CandidateRejectionMemoPath "${payload_id}")"
}
StagePlannedPayload() {
SELECTED_PAYLOAD_ID=""
SELECTED_PAYLOAD_DIR=""
SELECTED_PAYLOAD_METADATA=""
if [[ -n "${STAGED_PAYLOAD_ID}" ]]; then
Progress "Preparing Roblox..."
SELECTED_PAYLOAD_ID="${STAGED_PAYLOAD_ID}"
STAGED_PAYLOAD_ID=""
else
local -a fetch_arguments=(
--source "${SOURCE}"
--staging-root "${DOWNLOAD_STAGING_ROOT}"
)
[[ -z "${VERSION}" ]] || fetch_arguments+=(--version "${VERSION}")
SUPPORT_FAILURE_REASON=updater-download-failed
Progress "Downloading Roblox..."
Log "downloading provider payload"
local bundle_dir fetch_status=0
bundle_dir="$("${FETCH_SCRIPT}" "${fetch_arguments[@]}")" ||
fetch_status=$?
if (( fetch_status != 0 )); then
Log "provider download failed"
return "${fetch_status}"
fi
DOWNLOADED_BUNDLE_DIR="${bundle_dir}"
SUPPORT_FAILURE_REASON=updater-validation-failed
Progress "Installing Roblox..."
Log "validating downloaded x86_64 payload"
local validator_output validator_status=0
validator_output="$("${VALIDATOR_SCRIPT}" \
--base "${bundle_dir}/base.apk" \
--x86-64 "${bundle_dir}/split_config.x86_64.apk" \
--source "direct:${SOURCE}" --store-root "${DATA_ROOT}")" ||
validator_status=$?
if (( validator_status != 0 )); then
CleanupDownloadedBundle
Log "downloaded payload validation failed"
return "${validator_status}"
fi
SELECTED_PAYLOAD_ID="$(tail -n 1 <<<"${validator_output}")"
CleanupDownloadedBundle
fi
[[ "${SELECTED_PAYLOAD_ID}" =~ ^[0-9]+-[0-9a-f]{40}$ ]] || {
Log "validator returned an invalid payload ID: ${SELECTED_PAYLOAD_ID}"
return 21
}
SELECTED_PAYLOAD_DIR="${DATA_ROOT}/payloads/${SELECTED_PAYLOAD_ID}"
SELECTED_PAYLOAD_METADATA="${SELECTED_PAYLOAD_DIR}/roblox_payload.json"
[[ -f "${SELECTED_PAYLOAD_METADATA}" &&
! -L "${SELECTED_PAYLOAD_METADATA}" ]] || return 21
local selected_version_code metadata_version_code
selected_version_code="${SELECTED_PAYLOAD_ID%%-*}"
metadata_version_code="$(jq -er '.version_code |
select(type == "number" and floor == . and . >= 0) | tostring' \
"${SELECTED_PAYLOAD_METADATA}" 2>/dev/null || true)"
if [[ -z "${metadata_version_code}" ||
"${selected_version_code}" != "${metadata_version_code}" ]]; then
Log "validated payload metadata does not match its immutable payload ID"
return 22
fi
if [[ -n "${EXPECTED_PAYLOAD_VERSION_CODE}" &&
"${metadata_version_code}" != "${EXPECTED_PAYLOAD_VERSION_CODE}" ]]; then
Log "provider payload versionCode ${metadata_version_code} does not match expected ${EXPECTED_PAYLOAD_VERSION_CODE}; refusing probation"
return 22
fi
SUPPORT_CANDIDATE_METADATA="${SELECTED_PAYLOAD_METADATA}"
}
PromoteCanaryApprovedCandidate() {
local -r payload_id="$1"
local -r candidate_fingerprint="$2"
local -r baseline_payload_id="$3"
local -a store_arguments=(
--root "${DATA_ROOT}"
--compatibility "${COMPATIBILITY_PATH}"
--expected-current "${baseline_payload_id}"
--expected-payload-fingerprint "${candidate_fingerprint}"
)
local store_command=promote
if [[ "${CANDIDATE_IS_PROBATION}" == true ]]; then
(( ${#CANARY_ATTESTATIONS[@]} == 2 )) || return 6
store_arguments+=(
--candidate-profile "${CANDIDATE_PROFILE}"
--candidate-compatibility "${CANDIDATE_COMPATIBILITY}"
--runtime-fingerprint "${RUNTIME_FINGERPRINT}"
--runtime-build-id "${RUNTIME_BUILD_ID}"
--canary-attestation "${CANARY_ATTESTATIONS[0]}"
--canary-attestation "${CANARY_ATTESTATIONS[1]}"
)
store_command=promote-probation
fi
SUPPORT_FAILURE_REASON=updater-promotion-failed
Progress "Installing Roblox..."
Log "promoting canary-approved candidate"
if "${PAYLOAD_STORE_SCRIPT}" "${store_arguments[@]}" \
"${store_command}" "${payload_id}"; then
return 0
fi
local actual_payload_id
actual_payload_id="$(CurrentPayloadId)"
if [[ "${actual_payload_id}" == "${payload_id}" &&
"${baseline_payload_id}" != none ]]; then
Log "promotion reported failure after switching current; rolling back to ${baseline_payload_id}"
if ! "${PAYLOAD_STORE_SCRIPT}" --root "${DATA_ROOT}" \
--compatibility "${COMPATIBILITY_PATH}" \
--expected-current "${payload_id}" \
--expected-rollback-target "${baseline_payload_id}" rollback; then
Log "promotion and rollback to ${baseline_payload_id} both failed"
return 8
fi
actual_payload_id="$(CurrentPayloadId)"
fi
if [[ "${actual_payload_id}" == "${payload_id}" &&
"${baseline_payload_id}" == none ]]; then
local committed_payload_path committed_fingerprint verified_payload_id
committed_payload_path="$(jq -er '.payload_path |
select(type == "string")' "${DATA_ROOT}/current.json" 2>/dev/null ||
true)"
committed_fingerprint="$("${PAYLOAD_STORE_SCRIPT}" \
--root "${DATA_ROOT}" --compatibility "${COMPATIBILITY_PATH}" \
fingerprint "${payload_id}" 2>/dev/null || true)"
verified_payload_id="$(VerifiedCurrentPayloadId 2>/dev/null || true)"
[[ "${committed_payload_path}" == "payloads/${payload_id}" &&
"${committed_fingerprint}" == "${candidate_fingerprint}" &&
"${verified_payload_id}" == "${payload_id}" ]] ||
return 8
Log "initial promotion committed despite a nonzero store status" || true
return 0
fi
[[ "${actual_payload_id}" == "${baseline_payload_id}" ]] || return 8
Log "candidate ${payload_id} was not promoted; current payload ${baseline_payload_id} is preserved"
return 7
}
AttemptCandidate() {
local -r payload_id="$1"
local -r payload_dir="${DATA_ROOT}/payloads/${payload_id}"
local -r metadata="${payload_dir}/roblox_payload.json"
ResetCandidateProbation
CANDIDATE_NEEDS_REFERENCE=false
CANDIDATE_REJECTION_DEFERRED=false
SUPPORT_CANDIDATE_METADATA="${metadata}"
local baseline_payload_id
baseline_payload_id="$(CurrentPayloadId)"
LAST_ATTEMPT_BASELINE_PAYLOAD_ID="${baseline_payload_id}"
if ! IsSupportedCandidate "${metadata}"; then
local derive_status=0
DeriveCandidateProfile "${payload_dir}" "${payload_id}" ||
derive_status=$?
if (( derive_status != 0 )); then
[[ "${derive_status}" != 3 ]] || CANDIDATE_NEEDS_REFERENCE=true
return "${derive_status}"
fi
fi
BuildRuntime || return 22
ResolveCanaryRuntime || return 22
SUPPORT_FAILURE_REASON=updater-fingerprint-failed
Log "fingerprinting candidate payload"
local candidate_fingerprint
if ! candidate_fingerprint="$("${PAYLOAD_STORE_SCRIPT}" \
--root "${DATA_ROOT}" --compatibility "${COMPATIBILITY_PATH}" \
fingerprint "${payload_id}")"; then
return 23
fi
[[ "${candidate_fingerprint}" =~ ^[0-9a-f]{64}$ ]] || return 23
if [[ "${CANDIDATE_IS_PROBATION}" == true ]]; then
BuildCandidateRejectionContext "${payload_id}" \
"${candidate_fingerprint}" || return 23
if CandidateRejectionIsActive "${payload_id}"; then
Log "candidate ${payload_id} matches a recent failed canary; retry deferred for up to 6 hours"
CANDIDATE_REJECTION_DEFERRED=true
return 24
fi
fi
local canary_status=0
RunCandidateCanaries "${payload_dir}" "${payload_id}" \
"${candidate_fingerprint}" || canary_status=$?
if (( canary_status != 0 )); then
[[ "${CANDIDATE_IS_PROBATION}" != true ]] ||
RecordCandidateRejection "${payload_id}" "${canary_status}"
return "${canary_status}"
fi
PromoteCanaryApprovedCandidate "${payload_id}" "${candidate_fingerprint}" \
"${baseline_payload_id}" || return $?
ClearCandidateRejection "${payload_id}"
Log "candidate ${payload_id} passed readiness and is now current" || true
}
PreserveCurrentAfterUpdateFailure() {
local -r reason="$1"
local -r expected_payload_id="${2:-none}"
local actual_payload_id
actual_payload_id="$(CurrentPayloadId)"
if [[ "${actual_payload_id}" != "${expected_payload_id}" ]]; then
Log "${reason}; current payload is uncertain: expected ${expected_payload_id}, found ${actual_payload_id}"
return 2
fi
if [[ "${expected_payload_id}" != none ]]; then
CollectUpdaterSupportBundle 1
Log "${reason}; keeping the previous working payload"
if [[ "${LAUNCH}" == true ]]; then
BuildRuntime || return $?
LaunchCurrent
fi
return 0
fi
return 1
}
RollbackAfterApprovalRefreshFailure() {
local -r current_payload_id="${1:-}"
local -r previous_manifest="${DATA_ROOT}/previous_good.json"
local previous_payload_id="" previous_is_runnable=false
if [[ -f "${previous_manifest}" && ! -L "${previous_manifest}" ]]; then
previous_payload_id="$(jq -er '.payload_id |
select(type == "string" and test("^[0-9]+-[0-9a-f]{40}$"))' \
"${previous_manifest}" 2>/dev/null || true)"
fi
if [[ -n "${previous_payload_id}" ]]; then
local -r previous_metadata="${DATA_ROOT}/payloads/${previous_payload_id}/roblox_payload.json"
if IsSupportedCandidate "${previous_metadata}"; then
previous_is_runnable=true
elif BuildRuntime && ResolveCanaryRuntime; then
local previous_approval_path previous_generation=""
previous_approval_path="$(jq -r '.approval_path // empty' \
"${previous_manifest}" 2>/dev/null || true)"
if [[ "${previous_approval_path}" =~ ^approvals/${previous_payload_id}-([0-9a-f]{40})\.json$ ]]; then
previous_generation="${BASH_REMATCH[1]}"
fi
local previous_approval_file="${DATA_ROOT}/${previous_approval_path}"
if [[ -n "${previous_generation}" ]] &&
ApprovalMatchesResolvedRuntime "${previous_approval_file}"; then
previous_is_runnable=true
fi
fi
fi
if [[ "${previous_is_runnable}" == true ]]; then
Log "rolling back to validated ${previous_payload_id} after approval refresh failed"
local -a rollback_arguments=(
--root "${DATA_ROOT}"
--compatibility "${COMPATIBILITY_PATH}"
--expected-rollback-target "${previous_payload_id}"
)
if [[ "${current_payload_id}" =~ ^[0-9]+-[0-9a-f]{40}$ ]]; then
rollback_arguments+=(--expected-current "${current_payload_id}")
fi
if "${PAYLOAD_STORE_SCRIPT}" "${rollback_arguments[@]}" rollback &&
[[ "$(VerifiedCurrentPayloadId 2>/dev/null || true)" == \
"${previous_payload_id}" ]]; then
return 0
fi
fi
[[ "${current_payload_id}" =~ ^[0-9]+-[0-9a-f]{40}$ ]] || return 1
local -r current_version_code="${current_payload_id%%-*}"
STAGED_PAYLOAD_ID=""
local fallback_selection_status=0
FindNewestSupportedStagedCandidate 0 "${current_version_code}" ||
fallback_selection_status=$?
(( fallback_selection_status == 0 )) || return 1
local -r fallback_payload_id="${STAGED_PAYLOAD_ID}"
[[ "${fallback_payload_id}" != "${current_payload_id}" ]] || return 1
SUPPORT_FAILURE_REASON=updater-recovery-failed
local fallback_fingerprint
fallback_fingerprint="$("${PAYLOAD_STORE_SCRIPT}" \
--root "${DATA_ROOT}" --compatibility "${COMPATIBILITY_PATH}" \
fingerprint "${fallback_payload_id}")" || return 1
[[ "${fallback_fingerprint}" =~ ^[0-9a-f]{64}$ ]] || return 1
Log "activating exact-supported local fallback ${fallback_payload_id} after approval refresh failed"
"${PAYLOAD_STORE_SCRIPT}" --root "${DATA_ROOT}" \
--compatibility "${COMPATIBILITY_PATH}" \
--expected-current "${current_payload_id}" \
--expected-payload-fingerprint "${fallback_fingerprint}" \
promote "${fallback_payload_id}" || return 1
[[ "$(VerifiedCurrentPayloadId 2>/dev/null || true)" == \
"${fallback_payload_id}" ]]
}
RefreshApprovedCurrentForRuntime() {
if [[ ! -e "${DATA_ROOT}/current.json" &&
! -L "${DATA_ROOT}/current.json" ]]; then
return 0
fi
local current_payload_id current_payload_path approval_path
local profile_path compatibility_path
if ! current_payload_id="$(VerifiedCurrentPayloadId)"; then
Log "current payload failed integrity or approval verification"
RollbackAfterApprovalRefreshFailure || return 2
return 1
fi
[[ "${current_payload_id}" != none ]] || return 0
current_payload_path="$(jq -er '.payload_path' \
"${DATA_ROOT}/current.json" 2>/dev/null || true)"
[[ "${current_payload_path}" == "payloads/${current_payload_id}" ]] ||
return 2
local -r metadata="${DATA_ROOT}/${current_payload_path}/roblox_payload.json"
if IsSupportedCandidate "${metadata}"; then
if jq -e 'has("approval_path") or has("host_abi_profile_path") or
has("compatibility_manifest_path")' \
"${DATA_ROOT}/current.json" >/dev/null; then
Log "removing obsolete probation references from supported current payload"
"${PAYLOAD_STORE_SCRIPT}" --root "${DATA_ROOT}" \
--compatibility "${COMPATIBILITY_PATH}" \
--expected-current "${current_payload_id}" \
promote "${current_payload_id}" || return 2
[[ "$(VerifiedCurrentPayloadId 2>/dev/null || true)" == \
"${current_payload_id}" ]] || return 2
fi
return 0
fi
approval_path="$(jq -r '.approval_path // empty' \
"${DATA_ROOT}/current.json")"
profile_path="$(jq -r '.host_abi_profile_path // empty' \
"${DATA_ROOT}/current.json")"
compatibility_path="$(jq -r '.compatibility_manifest_path // empty' \
"${DATA_ROOT}/current.json")"
local approval_generation=""
if [[ "${approval_path}" =~ ^approvals/${current_payload_id}-([0-9a-f]{40})\.json$ ]]; then
approval_generation="${BASH_REMATCH[1]}"
fi
[[ -n "${approval_generation}" &&
"${profile_path}" == \
"host_abi_profiles/${current_payload_id}-${approval_generation}.json" &&
"${compatibility_path}" == \
"compatibility_profiles/${current_payload_id}-${approval_generation}.json" ]] ||
return 2
local -r approval_file="${DATA_ROOT}/${approval_path}"
local -r persisted_profile="${DATA_ROOT}/${profile_path}"
local -r persisted_compatibility="${DATA_ROOT}/${compatibility_path}"
local approval_artifact
for approval_artifact in "${approval_file}" "${persisted_profile}" \
"${persisted_compatibility}"; do
[[ -f "${approval_artifact}" && ! -L "${approval_artifact}" ]] ||
return 2
done
BuildRuntime || return 2
ResolveCanaryRuntime || return 2
ApprovalMatchesResolvedRuntime "${approval_file}" && return 0
Log "runtime identity changed; re-canarying approved payload ${current_payload_id}"
ResetCandidateProbation
PROBATION_ROOT="$(mktemp -d \
"${CACHE_ROOT}/.update-probation.${current_payload_id}.XXXXXX")"
CANDIDATE_PROFILE="${PROBATION_ROOT}/host_abi_profile.json"
CANDIDATE_COMPATIBILITY="${PROBATION_ROOT}/compatibility.json"
cp -- "${persisted_profile}" "${CANDIDATE_PROFILE}"
cp -- "${persisted_compatibility}" "${CANDIDATE_COMPATIBILITY}"
CANDIDATE_IS_PROBATION=true
SUPPORT_CANDIDATE_METADATA="${metadata}"
SUPPORT_FAILURE_REASON=updater-fingerprint-failed
local candidate_fingerprint
if ! candidate_fingerprint="$("${PAYLOAD_STORE_SCRIPT}" \
--root "${DATA_ROOT}" --compatibility "${COMPATIBILITY_PATH}" \
fingerprint "${current_payload_id}")" ||
[[ ! "${candidate_fingerprint}" =~ ^[0-9a-f]{64}$ ]]; then
RollbackAfterApprovalRefreshFailure "${current_payload_id}" || return 2
return 1
fi
BuildCandidateRejectionContext "${current_payload_id}" \
"${candidate_fingerprint}" || return 2
if CandidateRejectionIsActive "${current_payload_id}"; then
Log "runtime reapproval matches a recent failed canary; rolling back without retry"
RollbackAfterApprovalRefreshFailure "${current_payload_id}" || return 2
return 1
fi
local canary_status=0
RunCandidateCanaries "${DATA_ROOT}/${current_payload_path}" \
"${current_payload_id}" "${candidate_fingerprint}" ||
canary_status=$?
if (( canary_status != 0 )); then
RecordCandidateRejection "${current_payload_id}" "${canary_status}"
RollbackAfterApprovalRefreshFailure "${current_payload_id}" || return 2
return 1
fi
if ! PromoteCanaryApprovedCandidate "${current_payload_id}" \
"${candidate_fingerprint}" "${current_payload_id}"; then
RollbackAfterApprovalRefreshFailure "${current_payload_id}" || return 2
return 1
fi
ClearCandidateRejection "${current_payload_id}"
[[ "$(VerifiedCurrentPayloadId 2>/dev/null || true)" == \
"${current_payload_id}" ]] || return 2
Log "approval receipt refreshed for runtime Build ID ${RUNTIME_BUILD_ID}"
}
Main() {
SUPPORT_FAILURE_REASON=updater-dependency-failed
command -v jq >/dev/null 2>&1 || Die "jq is required"
command -v timeout >/dev/null 2>&1 || Die "GNU timeout is required"
command -v flock >/dev/null 2>&1 || Die "flock is required"
command -v readelf >/dev/null 2>&1 || Die "readelf is required"
SUPPORT_FAILURE_REASON=updater-config-failed
LoadYamlDefaults
ParseArguments "$@"
[[ "${SOURCE}" == apk-pure ]] || Die "update source must be apk-pure"
if [[ "${SCHEDULED}" == true &&
"$(jq -r 'if has("automatic") then .automatic else true end' \
<<<"${UPDATE_CONFIG_JSON}")" != true ]]; then
Log "scheduled update is disabled by ${CONFIG_FILE}"
return 0
fi
SUPPORT_FAILURE_REASON=updater-state-failed
mkdir -p "${CACHE_ROOT}/downloads" "${DATA_ROOT}" \
"${STATE_ROOT}/logs/updater"
UPDATER_LOG="$(mktemp "${STATE_ROOT}/logs/updater/update.XXXXXX.log")"
chmod 0600 -- "${UPDATER_LOG}"
AcquireUpdaterLock
if [[ "${STARTUP_PREFLIGHT}" == true ]] && SkipRemoteUpdateForStartup; then
return 0
fi
SUPPORT_FAILURE_REASON=updater-version-check-failed
if SkipDownloadWhenCurrent; then
Cleanup
if [[ "${LAUNCH}" == true && -f "${DATA_ROOT}/current.json" ]]; then
BuildRuntime
LaunchCurrent
fi
return 0
fi
local pre_stage_payload_id
pre_stage_payload_id="$(CurrentPayloadId)"
local stage_status=0
StagePlannedPayload || stage_status=$?
if (( stage_status != 0 )); then
PreserveCurrentAfterUpdateFailure "latest payload acquisition failed" \
"${pre_stage_payload_id}" &&
return 0
local bootstrap_ceiling=""
if [[ "${BOOTSTRAP_SELECTED_VERSION_CODE}" =~ ^[0-9]+$ ]]; then
(( 10#${BOOTSTRAP_SELECTED_VERSION_CODE} > 1 )) ||
return "${stage_status}"
bootstrap_ceiling="$((10#${BOOTSTRAP_SELECTED_VERSION_CODE} - 1))"
fi
stage_status=0
StageSupportedBootstrapFallback "${bootstrap_ceiling}" || stage_status=$?
(( stage_status == 0 )) || return "${stage_status}"
fi
local latest_payload_id="${SELECTED_PAYLOAD_ID}"
LATEST_STAGED_PAYLOAD_ID="${latest_payload_id}"
local attempt_status=0
AttemptCandidate "${latest_payload_id}" || attempt_status=$?
local -r latest_needs_reference="${CANDIDATE_NEEDS_REFERENCE}"
local -r latest_rejection_deferred="${CANDIDATE_REJECTION_DEFERRED}"
if (( attempt_status == 0 )); then
[[ "${LAUNCH}" == false ]] || LaunchCurrent
return 0
fi
local had_current=false
[[ -f "${DATA_ROOT}/current.json" &&
! -L "${DATA_ROOT}/current.json" ]] && had_current=true
if [[ "${latest_rejection_deferred}" == true &&
"${had_current}" == true ]]; then
Log "recently rejected candidate remains staged; keeping the current payload"
if [[ "${LAUNCH}" == true ]]; then
BuildRuntime || return $?
LaunchCurrent
fi
return 0
fi
if [[ "${latest_needs_reference}" != true && "${had_current}" == true ]]; then
PreserveCurrentAfterUpdateFailure \
"latest candidate ${latest_payload_id} failed probation" \
"${LAST_ATTEMPT_BASELINE_PAYLOAD_ID}"
return 0
fi
local candidate_version="${latest_payload_id%%-*}"
local fallback_ceiling=""
if [[ "${candidate_version}" =~ ^[0-9]+$ ]] &&
(( 10#${candidate_version} > 1 )); then
fallback_ceiling="$((10#${candidate_version} - 1))"
fi
if ! LatestSupportedBootstrapProfile "${fallback_ceiling}" \
>/dev/null 2>&1; then
return "${attempt_status}"
fi
Log "installing an exact-supported seed after latest candidate probation failed"
local pre_fallback_payload_id
pre_fallback_payload_id="$(CurrentPayloadId)"
local fallback_stage_status=0
StageSupportedBootstrapFallback "${fallback_ceiling}" ||
fallback_stage_status=$?
if (( fallback_stage_status != 0 )); then
PreserveCurrentAfterUpdateFailure "supported seed acquisition failed" \
"${pre_fallback_payload_id}" &&
return 0
return "${fallback_stage_status}"
fi
local fallback_payload_id="${SELECTED_PAYLOAD_ID}"
local fallback_status=0
AttemptCandidate "${fallback_payload_id}" || fallback_status=$?
if (( fallback_status != 0 )); then
PreserveCurrentAfterUpdateFailure "supported seed failed readiness" \
"${LAST_ATTEMPT_BASELINE_PAYLOAD_ID}" &&
return 0
return "${fallback_status}"
fi
if [[ "${latest_needs_reference}" == true ]] &&
[[ "${latest_payload_id}" != "${fallback_payload_id}" ]]; then
Log "retrying staged latest candidate with the verified seed profile"
attempt_status=0
AttemptCandidate "${latest_payload_id}" || attempt_status=$?
if (( attempt_status != 0 )); then
PreserveCurrentAfterUpdateFailure \
"latest candidate retry failed probation" \
"${LAST_ATTEMPT_BASELINE_PAYLOAD_ID}"
return 0
fi
fi
[[ "${LAUNCH}" == false ]] || LaunchCurrent
}
trap 'OnExit $?' EXIT
Main "$@"