mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 15:00:47 +02:00
1 parent
6f5de3a31d
commit
23fe2eeca5
20 files changed
+378
-392
No files matched your search
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2026 komaruworld
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.DEFAULT_GOAL := all
|
||||
.PHONY: all build release debug test apk install register-url-handler release-runtime portable portable-test standalone appimage flatpak run-flatpak run run-smoke run-unlimited run-game run-input run-resize run-network auto-run run-gles run-angle update-roblox update-auto update-auto-launch install-updater payload-status payload-rollback support-bundle clean submodules help
|
||||
.PHONY: all build release debug test apk install register-url-handler release-runtime portable portable-test standalone appimage flatpak run-flatpak run run-smoke run-unlimited run-game run-input run-resize run-network auto-run run-gles run-angle update-roblox update-auto update-auto-launch payload-status payload-rollback support-bundle clean submodules help
|
||||
|
||||
BUILD_DIR := build
|
||||
BINARY := $(BUILD_DIR)/mocktail
|
||||
@@ -141,9 +141,6 @@ update-auto-launch: build ## Update Roblox and launch the verified current paylo
|
||||
@./$(BUILD_DIR)/mocktail_updater update
|
||||
@./$(BINARY)
|
||||
|
||||
install-updater: ## Install and enable the six-hour systemd user update timer
|
||||
@./scripts/install_user_updater.sh
|
||||
|
||||
payload-status: build ## Show current and previous-good immutable payloads
|
||||
@./$(BUILD_DIR)/mocktail_updater status
|
||||
|
||||
|
||||
@@ -112,6 +112,11 @@ updates:
|
||||
# backend, and promote only on success. An existing current payload is
|
||||
# preserved when probation fails.
|
||||
automatic: true
|
||||
# Boolean (default: true): testing track. Always try provider latest, even
|
||||
# when it has no verified payload profile, and do not download or activate
|
||||
# an older supported payload in its place. Exact HostAbi derivation and two
|
||||
# canaries are still required before latest can become current.
|
||||
testing_latest_only: true
|
||||
# String (default: apk-pure): direct x86_64 APK provider.
|
||||
source: apk-pure
|
||||
# Reserved for desktop update integrations. `mocktail_updater` itself never
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
[Unit]
|
||||
Description=Check, canary, and install the latest working Roblox for Mocktail
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=%h/.config/mocktail/updater.env
|
||||
ExecStart=/usr/bin/env ${MOCKTAIL_UPDATE_HELPER} update
|
||||
Nice=10
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=Periodically check Roblox updates for Mocktail
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5m
|
||||
OnUnitActiveSec=6h
|
||||
RandomizedDelaySec=15m
|
||||
Persistent=true
|
||||
Unit=mocktail-update.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -53,10 +53,8 @@ struct CgroupMemoryLimitResult {
|
||||
bool active() const { return status == CgroupMemoryLimitStatus::kActive; }
|
||||
};
|
||||
|
||||
// Replaces the current process with the same command inside a transient
|
||||
// systemd user scope when cgroup v2 delegation is available. A successful
|
||||
// re-exec does not return. Systems without systemd continue with the watchdog
|
||||
// fallback and receive kUnavailable.
|
||||
// Compatibility hook for the former cgroup re-exec path. Automatic cgroup
|
||||
// scope creation is disabled, so callers continue with the watchdog fallback.
|
||||
CgroupMemoryLimitResult MaybeReexecWithCgroupMemoryLimit(
|
||||
int argc, char* const argv[], std::uint64_t limit_bytes,
|
||||
const std::vector<std::string>* reexec_arguments = nullptr);
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace mocktail::update {
|
||||
|
||||
struct UpdateConfig {
|
||||
bool automatic = true;
|
||||
bool testing_latest_only = true;
|
||||
bool launch_after_update = false;
|
||||
std::string source = "apk-pure";
|
||||
};
|
||||
|
||||
@@ -86,6 +86,7 @@ LAUNCH="${MOCKTAIL_UPDATE_LAUNCH:-false}"
|
||||
SKIP_BUILD=false
|
||||
SCHEDULED=false
|
||||
STARTUP_PREFLIGHT=false
|
||||
TESTING_LATEST_ONLY=true
|
||||
UPDATE_CONFIG_JSON="{}"
|
||||
STAGED_PAYLOAD_ID=""
|
||||
DOWNLOAD_STAGING_ROOT="${CACHE_ROOT}/downloads/apk-staging"
|
||||
@@ -113,7 +114,8 @@ Options:
|
||||
--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.
|
||||
remote updates, unless updates.testing_latest_only is
|
||||
enabled; provision one when none is available.
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
@@ -188,6 +190,8 @@ ParseArguments() {
|
||||
LoadYamlDefaults() {
|
||||
UPDATE_CONFIG_JSON="$(python3 "${SCRIPT_DIR}/read_update_config.py" "${CONFIG_FILE}")"
|
||||
[[ -n "${SOURCE}" ]] || SOURCE="$(jq -r '.source // "apk-pure"' <<<"${UPDATE_CONFIG_JSON}")"
|
||||
TESTING_LATEST_ONLY="$(jq -r '.testing_latest_only // true' \
|
||||
<<<"${UPDATE_CONFIG_JSON}")"
|
||||
if [[ "${LAUNCH}" != true &&
|
||||
"$(jq -r '.launch_after_update // false' <<<"${UPDATE_CONFIG_JSON}")" == true ]]; then
|
||||
LAUNCH=true
|
||||
@@ -562,6 +566,9 @@ SkipDownloadWhenCurrent() {
|
||||
Log "latest-version API check failed; preserving the runnable local payload"
|
||||
return 0
|
||||
fi
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
Die "updates.testing_latest_only requires available provider latest metadata"
|
||||
fi
|
||||
Log "latest-version API check failed and no runnable local payload exists; using the compatibility bootstrap profile"
|
||||
SelectSupportedBootstrapFallback
|
||||
return 1
|
||||
@@ -572,6 +579,9 @@ SkipDownloadWhenCurrent() {
|
||||
Log "latest-version API returned invalid metadata; preserving the runnable local payload"
|
||||
return 0
|
||||
fi
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
Die "updates.testing_latest_only requires valid provider latest metadata"
|
||||
fi
|
||||
Log "provider returned invalid version metadata and no runnable local payload exists; using the compatibility bootstrap profile"
|
||||
SelectSupportedBootstrapFallback
|
||||
return 1
|
||||
@@ -605,6 +615,11 @@ SkipRemoteUpdateForStartup() {
|
||||
Die "current payload is not runnable by this Mocktail runtime and rollback failed"
|
||||
fi
|
||||
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
Log "testing latest-only track enabled; checking provider latest during startup"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local current_payload_id
|
||||
current_payload_id="$(VerifiedCurrentPayloadId 2>/dev/null || true)"
|
||||
if [[ "${current_payload_id}" =~ ^[0-9]+-[0-9a-f]{40}$ ]]; then
|
||||
@@ -1551,6 +1566,9 @@ Main() {
|
||||
LoadYamlDefaults
|
||||
ParseArguments "$@"
|
||||
[[ "${SOURCE}" == apk-pure ]] || Die "update source must be apk-pure"
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true && -n "${VERSION}" ]]; then
|
||||
Die "--version cannot be combined with updates.testing_latest_only"
|
||||
fi
|
||||
if [[ "${SCHEDULED}" == true &&
|
||||
"$(jq -r 'if has("automatic") then .automatic else true end' \
|
||||
<<<"${UPDATE_CONFIG_JSON}")" != true ]]; then
|
||||
@@ -1585,6 +1603,10 @@ Main() {
|
||||
PreserveCurrentAfterUpdateFailure "latest payload acquisition failed" \
|
||||
"${pre_stage_payload_id}" &&
|
||||
return 0
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
Log "latest payload acquisition failed; testing latest-only track forbids a supported fallback"
|
||||
return "${stage_status}"
|
||||
fi
|
||||
local bootstrap_ceiling=""
|
||||
if [[ "${BOOTSTRAP_SELECTED_VERSION_CODE}" =~ ^[0-9]+$ ]]; then
|
||||
(( 10#${BOOTSTRAP_SELECTED_VERSION_CODE} > 1 )) ||
|
||||
@@ -1618,6 +1640,16 @@ Main() {
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [[ "${TESTING_LATEST_ONLY}" == true ]]; then
|
||||
if [[ "${had_current}" == true ]]; then
|
||||
PreserveCurrentAfterUpdateFailure \
|
||||
"latest candidate ${latest_payload_id} failed probation" \
|
||||
"${LAST_ATTEMPT_BASELINE_PAYLOAD_ID}"
|
||||
return 0
|
||||
fi
|
||||
Log "latest candidate ${latest_payload_id} failed probation; testing latest-only track forbids a supported fallback"
|
||||
return "${attempt_status}"
|
||||
fi
|
||||
if [[ "${latest_needs_reference}" != true && "${had_current}" == true ]]; then
|
||||
PreserveCurrentAfterUpdateFailure \
|
||||
"latest candidate ${latest_payload_id} failed probation" \
|
||||
|
||||
@@ -1088,9 +1088,13 @@ def validate_semantic_match(
|
||||
raise AnalyzerError("registry initializer object size is inconsistent")
|
||||
|
||||
|
||||
def find_signature_match(
|
||||
reference: ElfImage, candidate: ElfImage, spec: SignatureSpec
|
||||
) -> SignatureMatch:
|
||||
def find_signature_matches(
|
||||
reference: ElfImage,
|
||||
candidate: ElfImage,
|
||||
spec: SignatureSpec,
|
||||
*,
|
||||
allow_multiple_candidates: bool = False,
|
||||
) -> tuple[SignatureMatch, ...]:
|
||||
reference_instructions = disassemble(
|
||||
reference, spec.reference_rva, spec.instruction_count
|
||||
)
|
||||
@@ -1131,18 +1135,59 @@ def find_signature_match(
|
||||
):
|
||||
matches.append(signature_offset)
|
||||
search_offset = anchor_position + 1
|
||||
if not matches:
|
||||
raise AnalyzerError(
|
||||
f"signature {spec.name} matched 0 candidate locations"
|
||||
)
|
||||
if not allow_multiple_candidates and len(matches) != 1:
|
||||
raise AnalyzerError(
|
||||
f"signature {spec.name} matched {len(matches)} candidate locations"
|
||||
)
|
||||
semantic_matches = []
|
||||
semantic_errors = []
|
||||
for match in matches:
|
||||
match_rva = text.address + match - text.offset
|
||||
candidate_instructions = disassemble(
|
||||
candidate, match_rva, spec.instruction_count
|
||||
)
|
||||
try:
|
||||
validate_semantic_match(
|
||||
reference_instructions,
|
||||
candidate_instructions,
|
||||
spec.allow_registry_object_size_change,
|
||||
)
|
||||
except AnalyzerError as error:
|
||||
semantic_errors.append(error)
|
||||
continue
|
||||
semantic_matches.append(
|
||||
SignatureMatch(
|
||||
match_rva, reference_instructions, candidate_instructions
|
||||
)
|
||||
)
|
||||
if not semantic_matches and len(matches) == 1 and semantic_errors:
|
||||
raise semantic_errors[0]
|
||||
return tuple(semantic_matches)
|
||||
|
||||
|
||||
def find_signature_match(
|
||||
reference: ElfImage, candidate: ElfImage, spec: SignatureSpec
|
||||
) -> SignatureMatch:
|
||||
matches = find_signature_matches(reference, candidate, spec)
|
||||
if len(matches) != 1:
|
||||
raise AnalyzerError(
|
||||
f"signature {spec.name} matched {len(matches)} candidate locations"
|
||||
)
|
||||
match_rva = text.address + matches[0] - text.offset
|
||||
candidate_instructions = disassemble(candidate, match_rva, spec.instruction_count)
|
||||
validate_semantic_match(
|
||||
reference_instructions,
|
||||
candidate_instructions,
|
||||
spec.allow_registry_object_size_change,
|
||||
)
|
||||
return SignatureMatch(match_rva, reference_instructions, candidate_instructions)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def direct_call_target(instruction: Any, description: str) -> int:
|
||||
if (
|
||||
instruction.mnemonic != "call"
|
||||
or len(instruction.operands) != 1
|
||||
or instruction.operands[0].type != capstone_x86.X86_OP_IMM
|
||||
):
|
||||
raise AnalyzerError(f"{description} is not one direct call")
|
||||
return instruction.operands[0].imm
|
||||
|
||||
|
||||
def rip_targets(instruction: Any) -> tuple[int, ...]:
|
||||
@@ -1256,7 +1301,13 @@ def create_signature_specs(
|
||||
24,
|
||||
),
|
||||
SignatureSpec(
|
||||
"usable-size", parse_rva(bridges["usable-size"]["rva"], "usable size"), 24
|
||||
# This bridge is an 18-instruction leaf. Keeping the signature
|
||||
# bounded to the leaf avoids coupling it to padding or the next
|
||||
# function, which changed from executable code to INT3 padding in
|
||||
# Roblox versionCode 2904.
|
||||
"usable-size",
|
||||
parse_rva(bridges["usable-size"]["rva"], "usable size"),
|
||||
18,
|
||||
),
|
||||
SignatureSpec(
|
||||
"reallocate", parse_rva(bridges["reallocate"]["rva"], "reallocate"), 24
|
||||
@@ -1277,8 +1328,12 @@ def create_signature_specs(
|
||||
),
|
||||
SignatureSpec(
|
||||
"allocator-thread-initializer",
|
||||
parse_rva(data_seeds["allocator_thread_initializer"], "thread initializer"),
|
||||
24,
|
||||
parse_rva(
|
||||
data_seeds["allocator_thread_initializer"], "thread initializer"
|
||||
),
|
||||
# The wrapper ends at instruction 19. Later instructions belong
|
||||
# to an unrelated adjacent initializer.
|
||||
19,
|
||||
),
|
||||
SignatureSpec(
|
||||
"registry-initializer",
|
||||
@@ -1362,10 +1417,47 @@ def derive_profile(
|
||||
|
||||
reference_profile = reference_sidecar["profile"]
|
||||
anchors = reference_sidecar["derivation_anchors"]
|
||||
matches = {
|
||||
spec.name: find_signature_match(reference, candidate, spec)
|
||||
for spec in create_signature_specs(reference_profile, anchors)
|
||||
}
|
||||
matches = {}
|
||||
allocate_matches = ()
|
||||
for spec in create_signature_specs(reference_profile, anchors):
|
||||
if spec.name == "allocate":
|
||||
allocate_matches = find_signature_matches(
|
||||
reference, candidate, spec, allow_multiple_candidates=True
|
||||
)
|
||||
else:
|
||||
matches[spec.name] = find_signature_match(reference, candidate, spec)
|
||||
|
||||
# The allocate signature alone is intentionally ambiguous in 2904. Resolve
|
||||
# it through an independent invariant: the registry initializer directly
|
||||
# calls the allocator wrapper that Mocktail must publish in the profile.
|
||||
registry_match = matches["registry-initializer"]
|
||||
reference_allocate = parse_rva(
|
||||
bridge_map(reference_profile)["allocate"]["rva"],
|
||||
"reference allocate",
|
||||
)
|
||||
if (
|
||||
direct_call_target(
|
||||
registry_match.reference_instructions[9],
|
||||
"reference registry allocator call",
|
||||
)
|
||||
!= reference_allocate
|
||||
):
|
||||
raise AnalyzerError(
|
||||
"reference registry initializer does not call its allocator bridge"
|
||||
)
|
||||
candidate_allocate = direct_call_target(
|
||||
registry_match.candidate_instructions[9],
|
||||
"candidate registry allocator call",
|
||||
)
|
||||
selected_allocate = tuple(
|
||||
match for match in allocate_matches if match.rva == candidate_allocate
|
||||
)
|
||||
if len(selected_allocate) != 1:
|
||||
raise AnalyzerError(
|
||||
"signature allocate candidates do not identify exactly one "
|
||||
"registry allocator call"
|
||||
)
|
||||
matches["allocate"] = selected_allocate[0]
|
||||
reference_init = reference.init_array_relocations()
|
||||
candidate_init = candidate.init_array_relocations()
|
||||
if len(reference_init) != reference_profile["init_array_count"]:
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
|
||||
CONFIG_ROOT="${XDG_CONFIG_HOME:-${HOME}/.config}/mocktail"
|
||||
UNIT_ROOT="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
|
||||
CANARY_BINARY="${MOCKTAIL_UPDATE_CANARY_BIN:-}"
|
||||
UPDATE_HELPER="${MOCKTAIL_UPDATE_HELPER:-}"
|
||||
|
||||
if [[ -z "${CANARY_BINARY}" ]]; then
|
||||
for candidate in "${PROJECT_ROOT}/build/mocktail" \
|
||||
/usr/lib/mocktail/mocktail; do
|
||||
if [[ -f "${candidate}" && ! -L "${candidate}" && -x "${candidate}" ]]; then
|
||||
CANARY_BINARY="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ -z "${UPDATE_HELPER}" ]]; then
|
||||
for candidate in "${PROJECT_ROOT}/build/mocktail_updater" \
|
||||
/usr/lib/mocktail/mocktail_updater; do
|
||||
if [[ -f "${candidate}" && ! -L "${candidate}" && -x "${candidate}" ]]; then
|
||||
UPDATE_HELPER="${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[[ "${CANARY_BINARY}" == /* && -f "${CANARY_BINARY}" &&
|
||||
! -L "${CANARY_BINARY}" && -x "${CANARY_BINARY}" ]] || {
|
||||
echo "build or install Mocktail before enabling its update timer" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "${UPDATE_HELPER}" == /* && -f "${UPDATE_HELPER}" &&
|
||||
! -L "${UPDATE_HELPER}" && -x "${UPDATE_HELPER}" ]] || {
|
||||
echo "build or install the native Mocktail updater before enabling its timer" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "${PROJECT_ROOT}" != *$'\n'* && "${PROJECT_ROOT}" != *'%'* ]] || {
|
||||
echo "unsupported project path for systemd environment file" >&2
|
||||
exit 1
|
||||
}
|
||||
mkdir -p "${CONFIG_ROOT}" "${UNIT_ROOT}"
|
||||
install -m 0644 "${PROJECT_ROOT}/config/systemd/mocktail-update.service" \
|
||||
"${UNIT_ROOT}/mocktail-update.service"
|
||||
install -m 0644 "${PROJECT_ROOT}/config/systemd/mocktail-update.timer" \
|
||||
"${UNIT_ROOT}/mocktail-update.timer"
|
||||
if [[ ! -e "${CONFIG_ROOT}/config.yaml" ]]; then
|
||||
install -m 0600 "${PROJECT_ROOT}/config/mocktail.example.yaml" \
|
||||
"${CONFIG_ROOT}/config.yaml"
|
||||
fi
|
||||
temporary="$(mktemp "${CONFIG_ROOT}/.updater.env.XXXXXX")"
|
||||
trap 'rm -f -- "${temporary}"' EXIT
|
||||
printf 'MOCKTAIL_UPDATE_HELPER=%q\n' "${UPDATE_HELPER}" > "${temporary}"
|
||||
printf 'MOCKTAIL_UPDATE_CANARY_BIN=%q\n' "${CANARY_BINARY}" >> "${temporary}"
|
||||
chmod 0600 "${temporary}"
|
||||
mv -f -- "${temporary}" "${CONFIG_ROOT}/updater.env"
|
||||
trap - EXIT
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now mocktail-update.timer
|
||||
systemctl --user list-timers mocktail-update.timer --no-pager
|
||||
@@ -105,13 +105,14 @@ def main() -> None:
|
||||
fail("updates must be a mapping")
|
||||
allowed = {
|
||||
"automatic",
|
||||
"testing_latest_only",
|
||||
"source",
|
||||
"launch_after_update",
|
||||
}
|
||||
unknown = set(updates) - allowed
|
||||
if unknown:
|
||||
fail(f"unknown updates key: {sorted(unknown)[0]}")
|
||||
for key in ("automatic", "launch_after_update"):
|
||||
for key in ("automatic", "testing_latest_only", "launch_after_update"):
|
||||
if key in updates and not isinstance(updates[key], bool):
|
||||
fail(f"updates.{key} must be true or false")
|
||||
if "source" in updates and updates["source"] not in SUPPORTED_SOURCES:
|
||||
|
||||
@@ -18,8 +18,13 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
|
||||
#if defined(__GLIBC__)
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
namespace mocktail::compat {
|
||||
namespace {
|
||||
|
||||
@@ -31,6 +36,34 @@ struct ThreadStartContext {
|
||||
|
||||
std::atomic<NativeThreadInitializer> g_thread_initializer{nullptr};
|
||||
|
||||
size_t HostStackSizeForGuest(size_t guest_stack_size) noexcept {
|
||||
#if defined(__GLIBC__)
|
||||
using GetStaticTlsInfo = void (*)(size_t*, size_t*);
|
||||
static const auto get_static_tls_info =
|
||||
reinterpret_cast<GetStaticTlsInfo>(
|
||||
dlsym(RTLD_DEFAULT, "_dl_get_tls_static_info"));
|
||||
size_t tls_size = 0;
|
||||
size_t tls_alignment = 0;
|
||||
if (get_static_tls_info != nullptr) {
|
||||
get_static_tls_info(&tls_size, &tls_alignment);
|
||||
}
|
||||
|
||||
// glibc places static TLS inside the requested mapping. Compensate only
|
||||
// unusually large blocks; Mocktail's compatibility TLS is several MiB.
|
||||
constexpr size_t kLargeStaticTlsThreshold = 1U * 1024U * 1024U;
|
||||
if (tls_size > kLargeStaticTlsThreshold && tls_alignment != 0 &&
|
||||
(tls_alignment & (tls_alignment - 1)) == 0 &&
|
||||
tls_size <= std::numeric_limits<size_t>::max() - (tls_alignment - 1)) {
|
||||
const size_t reserve =
|
||||
(tls_size + tls_alignment - 1) & ~(tls_alignment - 1);
|
||||
if (guest_stack_size <= std::numeric_limits<size_t>::max() - reserve) {
|
||||
return guest_stack_size + reserve;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return guest_stack_size;
|
||||
}
|
||||
|
||||
int CopySupportedThreadAttributes(const pthread_attr_t& source,
|
||||
pthread_attr_t* destination) noexcept {
|
||||
int detach_state = PTHREAD_CREATE_JOINABLE;
|
||||
@@ -62,7 +95,8 @@ int CopySupportedThreadAttributes(const pthread_attr_t& source,
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = pthread_attr_setstacksize(destination, stack_size);
|
||||
result = pthread_attr_setstacksize(destination,
|
||||
HostStackSizeForGuest(stack_size));
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
@@ -112,7 +146,8 @@ int CopyRequiredThreadAttributes(const pthread_attr_t& source,
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return pthread_attr_setstacksize(destination, stack_size);
|
||||
return pthread_attr_setstacksize(destination,
|
||||
HostStackSizeForGuest(stack_size));
|
||||
}
|
||||
|
||||
int ConfigureHostSafeStackFallback(const pthread_attr_t* source,
|
||||
@@ -139,9 +174,11 @@ int ConfigureHostSafeStackFallback(const pthread_attr_t* source,
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
const size_t guest_floor =
|
||||
std::max(guest_stack_size, kBionicLp64FallbackThreadStackSize);
|
||||
return pthread_attr_setstacksize(
|
||||
destination, std::max({guest_stack_size, host_default_stack_size,
|
||||
kBionicLp64FallbackThreadStackSize}));
|
||||
destination,
|
||||
std::max(host_default_stack_size, HostStackSizeForGuest(guest_floor)));
|
||||
}
|
||||
|
||||
void* RunGuestThread(void* raw_context) noexcept {
|
||||
@@ -183,8 +220,9 @@ int CreateBionicPthread(pthread_t* thread, const pthread_attr_t* attr,
|
||||
if (result == 0) {
|
||||
normalized_attr_initialized = true;
|
||||
if (attr == nullptr) {
|
||||
result = pthread_attr_setstacksize(&normalized_attr,
|
||||
kBionicLp64DefaultThreadStackSize);
|
||||
result = pthread_attr_setstacksize(
|
||||
&normalized_attr,
|
||||
HostStackSizeForGuest(kBionicLp64DefaultThreadStackSize));
|
||||
} else {
|
||||
result = CopySupportedThreadAttributes(*attr, &normalized_attr);
|
||||
}
|
||||
|
||||
+2
-5
@@ -293,9 +293,6 @@ int main(int argc, char* argv[]) {
|
||||
std::cout << " [memory] hard process-tree limit active: "
|
||||
<< runtime_config.config.performance().memory_limit_mb
|
||||
<< " MiB RAM, swap disabled\n";
|
||||
} else {
|
||||
std::cerr << " [memory] cgroup hard limit unavailable: "
|
||||
<< cgroup_limit.detail << "; using the RSS+swap watchdog\n";
|
||||
}
|
||||
}
|
||||
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
||||
@@ -308,8 +305,8 @@ int main(int argc, char* argv[]) {
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
// Register immediately after any cgroup re-exec and before starting helper
|
||||
// processes or loading the Android payload. libgamemode caches its shared
|
||||
// Register before starting helper processes or loading the Android payload.
|
||||
// libgamemode caches its shared
|
||||
// D-Bus connection, so the first use must happen in the final game process.
|
||||
const mocktail::runtime::GameModePolicy game_mode_policy =
|
||||
runtime_config.config.performance().game_mode;
|
||||
|
||||
@@ -552,10 +552,12 @@ DiscordRpcActivity BuildDiscordRpcActivity(
|
||||
activity.details = TruncateUtf8(config.text.browsing, 128);
|
||||
break;
|
||||
case RobloxExperiencePresencePhase::kJoining:
|
||||
activity.details = TruncateUtf8(config.text.joining, 128);
|
||||
break;
|
||||
case RobloxExperiencePresencePhase::kPlaying: {
|
||||
if (place_name.empty()) {
|
||||
if (phase == RobloxExperiencePresencePhase::kJoining) {
|
||||
activity.details = TruncateUtf8(config.text.joining, 128);
|
||||
break;
|
||||
}
|
||||
place_name = config.text.unknown_place;
|
||||
}
|
||||
if (config.show_place_name) {
|
||||
@@ -564,14 +566,16 @@ DiscordRpcActivity BuildDiscordRpcActivity(
|
||||
activity.details = activity.state;
|
||||
activity.state.clear();
|
||||
}
|
||||
if (config.show_elapsed_time && session_started_at > 0) {
|
||||
if (phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
config.show_elapsed_time && session_started_at > 0) {
|
||||
activity.start_timestamp = session_started_at;
|
||||
}
|
||||
if (IsSafeExternalImageUrl(place_icon_url)) {
|
||||
activity.large_image = std::move(place_icon_url);
|
||||
activity.large_text = TruncateUtf8(place_name, 128);
|
||||
}
|
||||
if (config.join_enabled && request != nullptr &&
|
||||
if (phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
config.join_enabled && request != nullptr &&
|
||||
(!config.public_servers_only || IsPublicDiscordJoin(*request))) {
|
||||
activity.button_url = BuildDiscordJoinUrl(*request);
|
||||
if (!activity.button_url.empty()) {
|
||||
@@ -733,7 +737,8 @@ class DiscordRpcSession::Impl final {
|
||||
std::string place_name;
|
||||
std::string place_icon_url;
|
||||
bool resolve_metadata_after_publish = false;
|
||||
if (desired.phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
if ((desired.phase == RobloxExperiencePresencePhase::kJoining ||
|
||||
desired.phase == RobloxExperiencePresencePhase::kPlaying) &&
|
||||
desired.request.place_id > 0) {
|
||||
CachedPlaceMetadata& cached =
|
||||
place_metadata_[desired.request.place_id];
|
||||
@@ -741,7 +746,6 @@ class DiscordRpcSession::Impl final {
|
||||
place_icon_url = cached.icon_url;
|
||||
if ((cached.name.empty() || cached.icon_url.empty()) &&
|
||||
std::chrono::steady_clock::now() >= cached.next_attempt) {
|
||||
// Publish fallback activity while metadata is in flight.
|
||||
resolve_metadata_after_publish = true;
|
||||
}
|
||||
}
|
||||
@@ -761,7 +765,12 @@ class DiscordRpcSession::Impl final {
|
||||
std::fprintf(stderr, " [discord-rpc] connected to Discord Desktop\n");
|
||||
published_revision = 0;
|
||||
}
|
||||
if (published_revision != desired.revision) {
|
||||
const bool wait_for_place_metadata =
|
||||
(desired.phase == RobloxExperiencePresencePhase::kJoining ||
|
||||
desired.phase == RobloxExperiencePresencePhase::kPlaying) &&
|
||||
desired.request.place_id > 0 && place_name.empty();
|
||||
if (published_revision != desired.revision &&
|
||||
!wait_for_place_metadata) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
while (!activity_updates.empty() &&
|
||||
now - activity_updates.front() >= kDiscordActivityRateWindow) {
|
||||
@@ -824,7 +833,9 @@ class DiscordRpcSession::Impl final {
|
||||
if (Snapshot().revision != desired.revision) {
|
||||
continue;
|
||||
}
|
||||
if (changed) {
|
||||
if (changed &&
|
||||
(desired.phase == RobloxExperiencePresencePhase::kJoining ||
|
||||
desired.phase == RobloxExperiencePresencePhase::kPlaying)) {
|
||||
// Publish cached metadata through the normal rate limiter.
|
||||
published_revision = 0;
|
||||
continue;
|
||||
|
||||
+4
-162
@@ -15,7 +15,6 @@
|
||||
#include "runtime/memory_limit.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
@@ -27,9 +26,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
@@ -38,7 +35,6 @@ namespace {
|
||||
constexpr std::uint64_t kBytesPerKibibyte = 1024U;
|
||||
constexpr std::uint64_t kBytesPerMebibyte = 1024U * 1024U;
|
||||
constexpr int kWatchdogPollMilliseconds = 100;
|
||||
constexpr char kCgroupMarker[] = "MOCKTAIL_MEMORY_CGROUP_LIMIT_BYTES";
|
||||
|
||||
void WriteAllBestEffort(int descriptor, std::string_view bytes) {
|
||||
std::size_t offset = 0;
|
||||
@@ -115,90 +111,6 @@ ProcessMemoryUsage ReadCurrentProcessMemory() {
|
||||
std::string_view(buffer.data(), static_cast<std::size_t>(total)));
|
||||
}
|
||||
|
||||
bool ReadTextFile(const std::string& path, std::string* value) {
|
||||
std::ifstream input(path);
|
||||
if (!input) {
|
||||
return false;
|
||||
}
|
||||
return static_cast<bool>(input >> *value);
|
||||
}
|
||||
|
||||
bool ReadUnifiedCgroupPath(std::string* path) {
|
||||
std::ifstream input("/proc/self/cgroup");
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (line.rfind("0::", 0) == 0) {
|
||||
*path = line.substr(3);
|
||||
return !path->empty() && path->front() == '/';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ActiveCgroupMatches(std::uint64_t expected_bytes,
|
||||
std::string* detail) {
|
||||
std::string cgroup_path;
|
||||
if (!ReadUnifiedCgroupPath(&cgroup_path)) {
|
||||
*detail = "cannot resolve the current unified cgroup";
|
||||
return false;
|
||||
}
|
||||
const std::string root = "/sys/fs/cgroup" + cgroup_path;
|
||||
std::string memory_max;
|
||||
std::string swap_max;
|
||||
if (!ReadTextFile(root + "/memory.max", &memory_max) ||
|
||||
!ReadTextFile(root + "/memory.swap.max", &swap_max)) {
|
||||
*detail = "cannot read current cgroup memory limits";
|
||||
return false;
|
||||
}
|
||||
std::uint64_t configured_bytes = 0;
|
||||
const std::from_chars_result parsed = std::from_chars(
|
||||
memory_max.data(), memory_max.data() + memory_max.size(),
|
||||
configured_bytes);
|
||||
if (parsed.ec != std::errc() ||
|
||||
parsed.ptr != memory_max.data() + memory_max.size() ||
|
||||
configured_bytes > expected_bytes) {
|
||||
*detail = "current cgroup memory.max does not match the requested limit";
|
||||
return false;
|
||||
}
|
||||
if (swap_max != "0") {
|
||||
*detail = "current cgroup still permits swap";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CurrentExecutable(char* const argv[]) {
|
||||
std::array<char, PATH_MAX + 1> executable{};
|
||||
const ssize_t length =
|
||||
readlink("/proc/self/exe", executable.data(), PATH_MAX);
|
||||
if (length > 0) {
|
||||
executable[static_cast<std::size_t>(length)] = '\0';
|
||||
return executable.data();
|
||||
}
|
||||
return argv != nullptr && argv[0] != nullptr ? argv[0] : std::string();
|
||||
}
|
||||
|
||||
bool SystemdUserManagerAvailable(std::string* detail) {
|
||||
if (access("/sys/fs/cgroup/cgroup.controllers", R_OK) != 0) {
|
||||
*detail = "cgroup v2 is not mounted";
|
||||
return false;
|
||||
}
|
||||
if (access("/usr/bin/systemd-run", X_OK) != 0) {
|
||||
*detail = "systemd-run is not installed";
|
||||
return false;
|
||||
}
|
||||
const char* runtime_directory = getenv("XDG_RUNTIME_DIR");
|
||||
const std::string user_runtime =
|
||||
runtime_directory != nullptr && runtime_directory[0] != '\0'
|
||||
? runtime_directory
|
||||
: "/run/user/" + std::to_string(geteuid());
|
||||
if (access((user_runtime + "/systemd/private").c_str(), F_OK) != 0) {
|
||||
*detail = "systemd user manager is not running";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ProcessMemoryUsage ParseProcessStatusMemory(std::string_view status) {
|
||||
@@ -221,84 +133,14 @@ CgroupMemoryLimitResult MaybeReexecWithCgroupMemoryLimit(
|
||||
int argc, char* const argv[], std::uint64_t limit_bytes,
|
||||
const std::vector<std::string>* reexec_arguments) {
|
||||
CgroupMemoryLimitResult result;
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
(void)reexec_arguments;
|
||||
if (limit_bytes == 0) {
|
||||
result.detail = "memory limit is disabled";
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* marker = getenv(kCgroupMarker);
|
||||
if (marker != nullptr) {
|
||||
std::uint64_t marked_bytes = 0;
|
||||
const std::string_view marker_value(marker);
|
||||
const std::from_chars_result parsed = std::from_chars(
|
||||
marker_value.data(), marker_value.data() + marker_value.size(),
|
||||
marked_bytes);
|
||||
if (parsed.ec == std::errc() &&
|
||||
parsed.ptr == marker_value.data() + marker_value.size() &&
|
||||
marked_bytes == limit_bytes &&
|
||||
ActiveCgroupMatches(limit_bytes, &result.detail)) {
|
||||
result.status = CgroupMemoryLimitStatus::kActive;
|
||||
result.detail = "cgroup v2 MemoryMax is active and swap is disabled";
|
||||
} else if (result.detail.empty()) {
|
||||
result.detail = "cgroup activation marker is invalid";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!SystemdUserManagerAvailable(&result.detail)) {
|
||||
return result;
|
||||
}
|
||||
const std::string executable = CurrentExecutable(argv);
|
||||
if (executable.empty()) {
|
||||
result.status = CgroupMemoryLimitStatus::kExecFailed;
|
||||
result.detail = "cannot resolve the Mocktail executable";
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::string limit = std::to_string(limit_bytes);
|
||||
std::vector<std::string> arguments;
|
||||
arguments.reserve((reexec_arguments != nullptr
|
||||
? reexec_arguments->size()
|
||||
: static_cast<std::size_t>(argc)) +
|
||||
10U);
|
||||
arguments.emplace_back("/usr/bin/systemd-run");
|
||||
arguments.emplace_back("--user");
|
||||
arguments.emplace_back("--scope");
|
||||
arguments.emplace_back("--quiet");
|
||||
arguments.emplace_back("--same-dir");
|
||||
arguments.emplace_back("--expand-environment=no");
|
||||
arguments.emplace_back("--property=MemoryMax=" + limit);
|
||||
arguments.emplace_back("--property=MemorySwapMax=0");
|
||||
arguments.emplace_back("--");
|
||||
arguments.push_back(executable);
|
||||
if (reexec_arguments != nullptr) {
|
||||
arguments.insert(arguments.end(), reexec_arguments->begin(),
|
||||
reexec_arguments->end());
|
||||
} else {
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
arguments.emplace_back(argv[index]);
|
||||
}
|
||||
}
|
||||
std::vector<char*> exec_arguments;
|
||||
exec_arguments.reserve(arguments.size() + 1U);
|
||||
for (std::string& argument : arguments) {
|
||||
exec_arguments.push_back(argument.data());
|
||||
}
|
||||
exec_arguments.push_back(nullptr);
|
||||
|
||||
if (setenv(kCgroupMarker, limit.c_str(), 1) != 0) {
|
||||
result.status = CgroupMemoryLimitStatus::kExecFailed;
|
||||
result.detail = std::string("cannot mark cgroup re-exec: ") +
|
||||
std::strerror(errno);
|
||||
return result;
|
||||
}
|
||||
(void)fflush(nullptr);
|
||||
execv(arguments.front().c_str(), exec_arguments.data());
|
||||
const int exec_error = errno;
|
||||
(void)unsetenv(kCgroupMarker);
|
||||
result.status = CgroupMemoryLimitStatus::kExecFailed;
|
||||
result.detail = std::string("cannot execute systemd-run: ") +
|
||||
std::strerror(exec_error);
|
||||
result.detail = "automatic cgroup scopes are disabled";
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +144,11 @@ updates:
|
||||
# backend, and promote only on success. An existing current payload is
|
||||
# preserved when probation fails.
|
||||
automatic: true
|
||||
# Boolean (default: true): testing track. Always try provider latest, even
|
||||
# when it has no verified payload profile, and do not download or activate
|
||||
# an older supported payload in its place. Exact HostAbi derivation and two
|
||||
# canaries are still required before latest can become current.
|
||||
testing_latest_only: true
|
||||
# String (default: apk-pure): direct x86_64 APK provider.
|
||||
source: apk-pure
|
||||
# Reserved for desktop update integrations. `mocktail_updater` itself never
|
||||
|
||||
+109
-13
@@ -18,6 +18,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
@@ -906,10 +907,10 @@ bool ValidateSemantics(const std::vector<Instruction>& reference,
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<SignatureMatch> FindSignatureMatch(
|
||||
std::optional<std::vector<SignatureMatch>> FindSignatureMatches(
|
||||
const ElfImage& reference, const ElfImage& candidate,
|
||||
const Disassembler& disassembler, const SignatureSpec& spec,
|
||||
std::string* error) {
|
||||
bool allow_multiple_candidates, std::string* error) {
|
||||
const auto reference_instructions = disassembler.Decode(
|
||||
reference, spec.reference_rva, spec.instruction_count, 4096, error);
|
||||
if (!reference_instructions.has_value()) return std::nullopt;
|
||||
@@ -995,21 +996,68 @@ std::optional<SignatureMatch> FindSignatureMatch(
|
||||
}
|
||||
search = found + 1;
|
||||
}
|
||||
if (matches.size() != 1) {
|
||||
if (matches.empty()) {
|
||||
*error = "signature " + spec.name + " matched " +
|
||||
"0 candidate locations";
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!allow_multiple_candidates && matches.size() != 1U) {
|
||||
*error = "signature " + spec.name + " matched " +
|
||||
std::to_string(matches.size()) + " candidate locations";
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::uint64_t match_rva = text->address + matches[0];
|
||||
const auto candidate_instructions = disassembler.Decode(
|
||||
candidate, match_rva, spec.instruction_count, 4096, error);
|
||||
if (!candidate_instructions.has_value() ||
|
||||
!ValidateSemantics(*reference_instructions, *candidate_instructions,
|
||||
spec.registry_size_may_change, error)) {
|
||||
std::vector<SignatureMatch> semantic_matches;
|
||||
std::string first_semantic_error;
|
||||
for (const std::size_t match : matches) {
|
||||
const std::uint64_t match_rva = text->address + match;
|
||||
std::string semantic_error;
|
||||
const auto candidate_instructions = disassembler.Decode(
|
||||
candidate, match_rva, spec.instruction_count, 4096, &semantic_error);
|
||||
if (!candidate_instructions.has_value() ||
|
||||
!ValidateSemantics(*reference_instructions, *candidate_instructions,
|
||||
spec.registry_size_may_change, &semantic_error)) {
|
||||
if (first_semantic_error.empty()) {
|
||||
first_semantic_error = std::move(semantic_error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
semantic_matches.push_back(SignatureMatch{
|
||||
match_rva, *reference_instructions, *candidate_instructions});
|
||||
}
|
||||
if (semantic_matches.empty() && matches.size() == 1U &&
|
||||
!first_semantic_error.empty()) {
|
||||
*error = std::move(first_semantic_error);
|
||||
return std::nullopt;
|
||||
}
|
||||
return SignatureMatch{match_rva, *reference_instructions,
|
||||
*candidate_instructions};
|
||||
return semantic_matches;
|
||||
}
|
||||
|
||||
std::optional<SignatureMatch> FindSignatureMatch(
|
||||
const ElfImage& reference, const ElfImage& candidate,
|
||||
const Disassembler& disassembler, const SignatureSpec& spec,
|
||||
std::string* error) {
|
||||
auto matches =
|
||||
FindSignatureMatches(reference, candidate, disassembler, spec, false,
|
||||
error);
|
||||
if (!matches.has_value()) return std::nullopt;
|
||||
if (matches->size() != 1U) {
|
||||
*error = "signature " + spec.name + " matched " +
|
||||
std::to_string(matches->size()) + " candidate locations";
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::move(matches->front());
|
||||
}
|
||||
|
||||
std::optional<std::uint64_t> DirectCallTarget(
|
||||
const Instruction& instruction, std::string_view description,
|
||||
std::string* error) {
|
||||
if (instruction.mnemonic != "call" || instruction.operands.size() != 1U ||
|
||||
instruction.operands[0].type != Operand::Type::kImmediate ||
|
||||
instruction.operands[0].value < 0) {
|
||||
*error = std::string(description) + " is not one direct call";
|
||||
return std::nullopt;
|
||||
}
|
||||
return static_cast<std::uint64_t>(instruction.operands[0].value);
|
||||
}
|
||||
|
||||
std::vector<std::uint64_t> RipTargets(const Instruction& instruction) {
|
||||
@@ -1182,7 +1230,9 @@ std::optional<std::vector<SignatureSpec>> SignatureSpecs(const Json& sidecar,
|
||||
for (const auto& [label, count] :
|
||||
std::array<std::pair<std::string_view, std::size_t>, 6>{
|
||||
{{"small-allocate", 24},
|
||||
{"usable-size", 24},
|
||||
// usable-size is an 18-instruction leaf. Do not include bytes from
|
||||
// the adjacent function or alignment padding in its signature.
|
||||
{"usable-size", 18},
|
||||
{"reallocate", 24},
|
||||
{"allocate", 24},
|
||||
{"aligned-allocate-direct", 24},
|
||||
@@ -1193,7 +1243,7 @@ std::optional<std::vector<SignatureSpec>> SignatureSpecs(const Json& sidecar,
|
||||
}
|
||||
if (!add("arena-initializer", seeds["arena_initializer"], 24) ||
|
||||
!add("allocator-thread-initializer",
|
||||
seeds["allocator_thread_initializer"], 24) ||
|
||||
seeds["allocator_thread_initializer"], 19) ||
|
||||
!add("registry-initializer", bootstrap["registry_initializer"], 20,
|
||||
true) ||
|
||||
!add("allocator-object-initializer",
|
||||
@@ -1290,12 +1340,58 @@ std::optional<std::pair<Json, Json>> DeriveDocuments(const ElfImage& reference,
|
||||
const auto specs = SignatureSpecs(sidecar, error);
|
||||
if (!specs.has_value()) return std::nullopt;
|
||||
std::map<std::string, SignatureMatch> matches;
|
||||
std::vector<SignatureMatch> allocate_matches;
|
||||
for (const SignatureSpec& spec : *specs) {
|
||||
if (spec.name == "allocate") {
|
||||
auto candidates = FindSignatureMatches(reference, candidate,
|
||||
disassembler, spec, true, error);
|
||||
if (!candidates.has_value()) return std::nullopt;
|
||||
allocate_matches = std::move(*candidates);
|
||||
continue;
|
||||
}
|
||||
auto match =
|
||||
FindSignatureMatch(reference, candidate, disassembler, spec, error);
|
||||
if (!match.has_value()) return std::nullopt;
|
||||
matches.emplace(spec.name, std::move(*match));
|
||||
}
|
||||
// 2904 contains two semantically equivalent allocate wrappers. Select the
|
||||
// one independently referenced by the registry initializer instead of
|
||||
// weakening signature uniqueness globally.
|
||||
const SignatureMatch& registry_match = matches.at("registry-initializer");
|
||||
if (allocate_matches.empty() || registry_match.reference.size() <= 9U ||
|
||||
registry_match.candidate.size() <= 9U) {
|
||||
*error = "allocator signatures are incomplete";
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto reference_allocate = DirectCallTarget(
|
||||
registry_match.reference[9], "reference registry allocator call", error);
|
||||
if (!reference_allocate.has_value() ||
|
||||
*reference_allocate !=
|
||||
allocate_matches.front().reference.front().address) {
|
||||
if (error->empty()) {
|
||||
*error =
|
||||
"reference registry initializer does not call its allocator bridge";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto candidate_allocate = DirectCallTarget(
|
||||
registry_match.candidate[9], "candidate registry allocator call", error);
|
||||
if (!candidate_allocate.has_value()) return std::nullopt;
|
||||
auto selected_allocate = std::find_if(
|
||||
allocate_matches.begin(), allocate_matches.end(),
|
||||
[&](const SignatureMatch& match) {
|
||||
return match.rva == *candidate_allocate;
|
||||
});
|
||||
if (selected_allocate == allocate_matches.end() ||
|
||||
std::find_if(std::next(selected_allocate), allocate_matches.end(),
|
||||
[&](const SignatureMatch& match) {
|
||||
return match.rva == *candidate_allocate;
|
||||
}) != allocate_matches.end()) {
|
||||
*error = "signature allocate candidates do not identify exactly one "
|
||||
"registry allocator call";
|
||||
return std::nullopt;
|
||||
}
|
||||
matches.emplace("allocate", std::move(*selected_allocate));
|
||||
const auto reference_init = reference.InitArray(error);
|
||||
const auto candidate_init = candidate.InitArray(error);
|
||||
if (!reference_init.has_value() || !candidate_init.has_value()) {
|
||||
|
||||
@@ -155,6 +155,11 @@ UpdateConfigResult LoadUpdateConfig(const std::filesystem::path& path) {
|
||||
result.error = "updates.automatic must be true or false";
|
||||
break;
|
||||
}
|
||||
} else if (key == "testing_latest_only") {
|
||||
if (!ParseBoolean(value, &result.config.testing_latest_only)) {
|
||||
result.error = "updates.testing_latest_only must be true or false";
|
||||
break;
|
||||
}
|
||||
} else if (key == "launch_after_update") {
|
||||
if (!ParseBoolean(value, &result.config.launch_after_update)) {
|
||||
result.error = "updates.launch_after_update must be true or false";
|
||||
|
||||
@@ -235,7 +235,8 @@ ReferenceProfile ResolveReference(const UpdatePaths& paths,
|
||||
ApkPureProvider* provider,
|
||||
PayloadStore* store,
|
||||
const std::filesystem::path& workspace,
|
||||
int progress_fd) {
|
||||
int progress_fd,
|
||||
bool allow_reference_download) {
|
||||
ReferenceProfile result;
|
||||
if (installed && !installed.host_abi_profile.empty()) {
|
||||
std::error_code filesystem_error;
|
||||
@@ -262,6 +263,12 @@ ReferenceProfile ResolveReference(const UpdatePaths& paths,
|
||||
result.profile = paths.host_abi_reference_profile;
|
||||
return result;
|
||||
}
|
||||
if (!allow_reference_download) {
|
||||
result.error =
|
||||
"updates.testing_latest_only requires an installed HostAbi reference; "
|
||||
"downloading an older reference payload is disabled";
|
||||
return result;
|
||||
}
|
||||
Candidate downloaded =
|
||||
DownloadCandidate(provider, store, ExactPayloadIdentity(preferred), true,
|
||||
paths, workspace / "reference", progress_fd);
|
||||
@@ -397,7 +404,9 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
|
||||
}
|
||||
ApkPureProvider provider;
|
||||
std::optional<ProviderVersion> latest_version;
|
||||
if (request.check_latest) {
|
||||
const bool check_latest =
|
||||
request.check_latest || configured.config.testing_latest_only;
|
||||
if (check_latest) {
|
||||
Progress(request.progress_fd, "Checking Roblox...");
|
||||
const ProviderVersion latest = provider.CheckLatest();
|
||||
if (latest) {
|
||||
@@ -414,11 +423,20 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
|
||||
result.message = "installed Roblox is newer than provider metadata";
|
||||
return result;
|
||||
}
|
||||
} else if (current) {
|
||||
result.payload_id = current.payload_id;
|
||||
result.message =
|
||||
"update metadata is temporarily unavailable: " + latest.error;
|
||||
return result;
|
||||
} else {
|
||||
if (current) {
|
||||
result.payload_id = current.payload_id;
|
||||
result.message =
|
||||
"update metadata is temporarily unavailable: " + latest.error;
|
||||
return result;
|
||||
}
|
||||
if (configured.config.testing_latest_only) {
|
||||
result.error =
|
||||
"updates.testing_latest_only requires available provider latest "
|
||||
"metadata: " +
|
||||
latest.error;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::filesystem::path workspace = UniqueDirectory(
|
||||
@@ -475,7 +493,8 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
|
||||
if (candidate && !candidate.exact_supported && candidate.profile.empty()) {
|
||||
const ReferenceProfile reference =
|
||||
ResolveReference(paths, installed, *preferred, &provider, &store,
|
||||
workspace, request.progress_fd);
|
||||
workspace, request.progress_fd,
|
||||
!configured.config.testing_latest_only);
|
||||
if (!reference) {
|
||||
candidate_error = reference.error;
|
||||
} else {
|
||||
@@ -530,6 +549,14 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (configured.config.testing_latest_only) {
|
||||
result.error = "latest Roblox failed probation (" + candidate_error +
|
||||
"); updates.testing_latest_only forbids activating an "
|
||||
"older supported fallback";
|
||||
cleanup();
|
||||
return result;
|
||||
}
|
||||
|
||||
Candidate fallback;
|
||||
const std::string preferred_id =
|
||||
std::to_string(preferred->version_code) + "-" + preferred->elf_build_id;
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly installer="${1:?user updater installer is required}"
|
||||
readonly project_root="${2:?project root is required}"
|
||||
test_root="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${test_root}"' EXIT
|
||||
|
||||
mkdir -p "${test_root}/bin" "${test_root}/home"
|
||||
canary="${test_root}/mocktail"
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' >"${canary}"
|
||||
chmod 0755 "${canary}"
|
||||
cat >"${test_root}/bin/systemctl" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' "$*" >>"${SYSTEMCTL_CALLS:?}"
|
||||
EOF
|
||||
chmod 0755 "${test_root}/bin/systemctl"
|
||||
|
||||
export HOME="${test_root}/home"
|
||||
export XDG_CONFIG_HOME="${test_root}/config"
|
||||
export MOCKTAIL_UPDATE_CANARY_BIN="${canary}"
|
||||
export SYSTEMCTL_CALLS="${test_root}/systemctl-calls"
|
||||
export PATH="${test_root}/bin:${PATH}"
|
||||
"${installer}" >/dev/null
|
||||
|
||||
readonly config_root="${XDG_CONFIG_HOME}/mocktail"
|
||||
readonly unit_root="${XDG_CONFIG_HOME}/systemd/user"
|
||||
[[ -f "${config_root}/config.yaml" ]]
|
||||
[[ -f "${unit_root}/mocktail-update.service" ]]
|
||||
[[ -f "${unit_root}/mocktail-update.timer" ]]
|
||||
grep -Fxq "MOCKTAIL_PROJECT_ROOT=${project_root}" \
|
||||
"${config_root}/updater.env"
|
||||
grep -Fxq "MOCKTAIL_UPDATE_CANARY_BIN=${canary}" \
|
||||
"${config_root}/updater.env"
|
||||
grep -Fq \
|
||||
'auto_update_roblox.sh --skip-build --no-launch --scheduled' \
|
||||
"${unit_root}/mocktail-update.service"
|
||||
grep -Fxq -- '--user daemon-reload' "${SYSTEMCTL_CALLS}"
|
||||
grep -Fxq -- '--user enable --now mocktail-update.timer' "${SYSTEMCTL_CALLS}"
|
||||
grep -Fxq -- '--user list-timers mocktail-update.timer --no-pager' \
|
||||
"${SYSTEMCTL_CALLS}"
|
||||
|
||||
# A source-tree timer must follow the runtime built from that same tree. An
|
||||
# older system installation may coexist, but it cannot safely approve or
|
||||
# reject payloads on behalf of the development binary.
|
||||
fixture_project="${test_root}/fixture-project"
|
||||
mkdir -p "${fixture_project}/scripts" "${fixture_project}/build" \
|
||||
"${fixture_project}/config/systemd"
|
||||
cp -- "${installer}" "${fixture_project}/scripts/install_user_updater.sh"
|
||||
cp -- "${project_root}/config/systemd/mocktail-update.service" \
|
||||
"${fixture_project}/config/systemd/mocktail-update.service"
|
||||
cp -- "${project_root}/config/systemd/mocktail-update.timer" \
|
||||
"${fixture_project}/config/systemd/mocktail-update.timer"
|
||||
cp -- "${project_root}/config/mocktail.example.yaml" \
|
||||
"${fixture_project}/config/mocktail.example.yaml"
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' > \
|
||||
"${fixture_project}/build/mocktail"
|
||||
chmod 0755 "${fixture_project}/build/mocktail"
|
||||
unset MOCKTAIL_UPDATE_CANARY_BIN
|
||||
"${fixture_project}/scripts/install_user_updater.sh" >/dev/null
|
||||
grep -Fxq \
|
||||
"MOCKTAIL_UPDATE_CANARY_BIN=${fixture_project}/build/mocktail" \
|
||||
"${config_root}/updater.env"
|
||||
|
||||
echo "user updater installation checks passed"
|
||||
Reference in new issue
Block a user