diff --git a/CMakeLists.txt b/CMakeLists.txt index 28d2dd3..8e7583a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -648,6 +648,11 @@ target_link_options(mocktail PRIVATE ) # Native FreeBSD helper for Linuxulator's unsupported DF socket option. +# The helper is x86-64 assembly; other host architectures have nothing to +# assemble. +if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") + set(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER OFF) +endif() option(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER "Build the native FreeBSD Linuxulator socket helper" ON) if(MOCKTAIL_BUILD_FREEBSD_SOCKET_HELPER) diff --git a/config/roblox_compatibility.json b/config/roblox_compatibility.json index a452816..b954362 100644 --- a/config/roblox_compatibility.json +++ b/config/roblox_compatibility.json @@ -63,6 +63,17 @@ }, "reason": "Verified 2026-08-28 on the current host: the machine-derived exact-Build-ID profile passed two isolated no-recovery Tier C Vulkan canaries; fullscreen state synchronization and Build-ID-scoped FmodAudioDevice output interposition were re-derived for the current payload; legacy executable patches and signal recovery remain disabled." }, + { + "version_name": "2.736.1408", + "version_code": 2998, + "elf_build_id": "ed2b035768b1513561d0e6ea7f9bbec35b0e60e5", + "status": "experimental", + "default_allowed": true, + "allow_legacy_binary_patches": false, + "allow_host_abi_bridges": true, + "allow_host_constructor_replay": true, + "reason": "AArch64 arm64-v8a payload of the same 2.736.1408 release as the verified x86-64 profile; allocator, constructor replay, and pre-JNI bootstrap RVAs were derived by cross-architecture semantic matching. Pending runtime validation." + }, { "version_name": "2.736.1408", "version_code": 2998, diff --git a/include/compat/bionic_abi_exports.h b/include/compat/bionic_abi_exports.h index 7305b87..2ee319d 100644 --- a/include/compat/bionic_abi_exports.h +++ b/include/compat/bionic_abi_exports.h @@ -33,6 +33,23 @@ void SetLegacyBionicDiagnosticsEnabled(bool enabled); // guest layout explicit instead of exposing the incompatible host libc type. using MocktailBionicMutexAttr = int64_t; +// Bionic LP64 pthread_attr_t: 56 bytes with explicit field offsets. The host +// glibc layout is opaque and wider on aarch64, so guest buffers must be read +// and written through this struct only. +struct MocktailBionicPthreadAttr { + std::uint32_t flags; + void* stack_base; + std::size_t stack_size; + std::size_t guard_size; + std::int32_t sched_policy; + std::int32_t sched_priority; + char reserved[16]; +}; +static_assert(sizeof(MocktailBionicPthreadAttr) == 56); + +inline constexpr std::uint32_t kMocktailBionicAttrFlagDetached = 0x1; +inline constexpr std::uint32_t kMocktailBionicAttrFlagUserStack = 0x2; + extern "C" { void mocktail_set_current_jni_env(void* env); @@ -84,6 +101,18 @@ int mocktail_pthread_barrier_init(pthread_barrier_t* barrier, int mocktail_pthread_barrier_destroy(pthread_barrier_t* barrier); int mocktail_pthread_barrier_wait(pthread_barrier_t* barrier); +int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr); +int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr); +int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr, + size_t stack_size); +int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr, + int detach_state); +int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr, + const struct sched_param* parameters); +int mocktail_pthread_getattr_np(MocktailBionicPthreadAttr* attr); +int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr, + void** stack_base, size_t* stack_size); + extern void* mocktail_gameactivity_on_start_native; extern void* mocktail_gameactivity_on_resume_native; extern void* mocktail_gameactivity_on_surface_created_native; diff --git a/include/compat/bionic_pthread_create_runtime.h b/include/compat/bionic_pthread_create_runtime.h index c499fc5..bcd5da6 100644 --- a/include/compat/bionic_pthread_create_runtime.h +++ b/include/compat/bionic_pthread_create_runtime.h @@ -25,11 +25,12 @@ void ConfigureBionicPthreadThreadInitializer( } // namespace mocktail::compat +struct MocktailBionicPthreadAttr; + // Rebuilds attributes instead of passing Android pthread_attr_t to glibc. -extern "C" int mocktail_bionic_pthread_create(pthread_t* thread, - const pthread_attr_t* attr, - void* (*start_routine)(void*), - void* argument); +extern "C" int mocktail_bionic_pthread_create( + pthread_t* thread, const MocktailBionicPthreadAttr* attr, + void* (*start_routine)(void*), void* argument); // Guest real-time scheduling is denied to avoid the host RLIMIT_RTTIME watchdog. extern "C" int mocktail_bionic_pthread_setschedparam( diff --git a/include/compat/guest_abi.h b/include/compat/guest_abi.h new file mode 100644 index 0000000..2330b2a --- /dev/null +++ b/include/compat/guest_abi.h @@ -0,0 +1,38 @@ +#ifndef MOCKTAIL_COMPAT_GUEST_ABI_H_ +#define MOCKTAIL_COMPAT_GUEST_ABI_H_ + +#include +#include + +namespace mocktail::compat { + +#if defined(__aarch64__) +// Android ABI name as it appears in APK lib/ directories and build metadata. +inline constexpr std::string_view kGuestAbi = "arm64-v8a"; +// APK split-config name carrying the guest native libraries. +inline constexpr std::string_view kGuestApkSplitName = "config.arm64_v8a"; +// File name the payload store uses for the split APK. +inline constexpr std::string_view kGuestSplitApkFile = + "split_config.arm64_v8a.apk"; +// sha256 map key holding the split APK digest in payload metadata. +inline constexpr std::string_view kGuestSplitApkHashKey = "arm64_v8a_split_apk"; +// Short CPU label for device metadata and user-facing strings. +inline constexpr std::string_view kGuestCpuName = "aarch64"; +inline constexpr int kGuestElfMachine = EM_AARCH64; +#elif defined(__x86_64__) +inline constexpr std::string_view kGuestAbi = "x86_64"; +inline constexpr std::string_view kGuestApkSplitName = "config.x86_64"; +inline constexpr std::string_view kGuestSplitApkFile = "split_config.x86_64.apk"; +inline constexpr std::string_view kGuestSplitApkHashKey = "x86_64_split_apk"; +inline constexpr std::string_view kGuestCpuName = "x86_64"; +inline constexpr int kGuestElfMachine = EM_X86_64; +#else +#error "mocktail only supports aarch64 and x86_64 hosts" +#endif + +// APK zip entry holding the guest libroblox.so. +inline constexpr std::string_view kGuestLibApkEntryPrefix = "lib/"; + +} // namespace mocktail::compat + +#endif // MOCKTAIL_COMPAT_GUEST_ABI_H_ diff --git a/include/jnivm/jnivm.h b/include/jnivm/jnivm.h index 2af5149..d0fd579 100644 --- a/include/jnivm/jnivm.h +++ b/include/jnivm/jnivm.h @@ -48,9 +48,15 @@ struct PlatformIdentity { std::string manufacturer = "Mocktail"; std::string model = "Mocktail Headless"; std::string brand = "Mocktail"; +#if defined(__aarch64__) + std::string device_code = "linux-arm64"; + std::string device_sku = "mocktail-arm64"; + std::string soc_model = "aarch64"; +#else std::string device_code = "linux-x86_64"; std::string device_sku = "mocktail-x86_64"; std::string soc_model = "x86_64"; +#endif }; struct RobloxCredentialView { diff --git a/include/mocktail/audio/fmod_thread_floating_point.h b/include/mocktail/audio/fmod_thread_floating_point.h index d42d0a1..f4132b1 100644 --- a/include/mocktail/audio/fmod_thread_floating_point.h +++ b/include/mocktail/audio/fmod_thread_floating_point.h @@ -6,6 +6,9 @@ #if defined(__x86_64__) #include #endif +#if defined(__aarch64__) +#include +#endif namespace mocktail::audio { @@ -22,16 +25,23 @@ class FmodThreadFloatingPointMode final { delete; bool Enable(const char* thread_name) noexcept { -#if defined(__x86_64__) +#if defined(__x86_64__) || defined(__aarch64__) if (active_ || thread_name == nullptr) return false; const std::string_view name(thread_name); if (name != "FMOD mixer thread" && name != "FMOD feeder thread" && name != "FMOD Convolution thread" && name != "FMOD Worker Thread") { return false; } +#if defined(__x86_64__) const unsigned int control = _mm_getcsr(); original_mode_ = control & kModeMask; _mm_setcsr(control | kModeMask); +#else + std::uint64_t control; + __asm__ volatile("mrs %0, fpcr" : "=r"(control)); + original_mode_ = control & kModeMask; + __asm__ volatile("msr fpcr, %0" ::"r"(control | kModeMask)); +#endif active_ = true; return true; #else @@ -41,11 +51,18 @@ class FmodThreadFloatingPointMode final { } void Restore() noexcept { -#if defined(__x86_64__) +#if defined(__x86_64__) || defined(__aarch64__) if (active_) { // Restore only our two bits, preserving rounding, exception masks and // status flags set by the guest while the thread was attached. +#if defined(__x86_64__) _mm_setcsr((_mm_getcsr() & ~kModeMask) | original_mode_); +#else + std::uint64_t control; + __asm__ volatile("mrs %0, fpcr" : "=r"(control)); + __asm__ volatile("msr fpcr, %0" ::"r"((control & ~kModeMask) | + original_mode_)); +#endif active_ = false; } #endif @@ -56,6 +73,13 @@ class FmodThreadFloatingPointMode final { static constexpr unsigned int kModeMask = (1U << 15) | (1U << 6); unsigned int original_mode_ = 0; bool active_ = false; +#elif defined(__aarch64__) + // FPCR FZ flushes single/double subnormals; FZ16 covers the half-precision + // path used by some voice DSP filters. + static constexpr std::uint64_t kModeMask = + (std::uint64_t{1} << 24) | (std::uint64_t{1} << 19); + std::uint64_t original_mode_ = 0; + bool active_ = false; #endif }; diff --git a/include/runtime/runtime_paths.h b/include/runtime/runtime_paths.h index f5d1a70..b55a4cd 100644 --- a/include/runtime/runtime_paths.h +++ b/include/runtime/runtime_paths.h @@ -14,7 +14,7 @@ struct ActivePayloadPaths { std::filesystem::path root; std::filesystem::path roblox_library; std::filesystem::path base_apk; - std::filesystem::path x86_64_split_apk; + std::filesystem::path arch_split_apk; std::filesystem::path assets_content; std::filesystem::path compatibility_manifest; std::filesystem::path host_abi_profile; diff --git a/scripts/verify_linux_libc_abi.sh b/scripts/verify_linux_libc_abi.sh index d1e9bde..a0841e9 100755 --- a/scripts/verify_linux_libc_abi.sh +++ b/scripts/verify_linux_libc_abi.sh @@ -110,8 +110,14 @@ for elf in "$@"; do fi machine="$(LC_ALL=C readelf --file-header "${elf}" 2>/dev/null | sed -n 's/^[[:space:]]*Machine:[[:space:]]*//p')" - if [[ "${machine}" != *X86-64* ]]; then - printf 'ABI check: %s is not x86-64 (%s)\n' "${elf}" "${machine}" >&2 + case "$(uname -m)" in + x86_64) expected_machine='X86-64' ;; + aarch64) expected_machine='AArch64' ;; + *) expected_machine='X86-64' ;; + esac + if [[ "${machine}" != *"${expected_machine}"* ]]; then + printf 'ABI check: %s is not %s (%s)\n' "${elf}" "${expected_machine}" \ + "${machine}" >&2 status=1 fi diff --git a/src/compat/bionic_abi_exports.cc b/src/compat/bionic_abi_exports.cc index 26b06ae..01614f0 100644 --- a/src/compat/bionic_abi_exports.cc +++ b/src/compat/bionic_abi_exports.cc @@ -16,6 +16,8 @@ #include #include +#include "compat/bionic_pthread_create_runtime.h" +#include "compat/guest_abi.h" #include "compat/http_client_spin_guard.h" #include "window/window.h" @@ -31,6 +33,14 @@ const char* EnvironmentOr(const char* name, const char* fallback) { return value != nullptr && value[0] != '\0' ? value : fallback; } +const char* GuestAbiPropertyValue() { +#if defined(__aarch64__) + return "arm64-v8a"; +#else + return "x86_64"; +#endif +} + int CallHostVsnprintf(char* dst, size_t count, const char* format, va_list args) { using HostVsnprintf = int (*)(char*, size_t, const char*, va_list); @@ -173,8 +183,12 @@ int __system_property_get(const char* name, char* value) { result = "35"; } else if (std::strcmp(name, "ro.build.version.release") == 0) { result = "15"; - } else if (std::strcmp(name, "ro.product.cpu.abi") == 0) { - result = "x86_64"; + } else if (std::strcmp(name, "ro.product.cpu.abi") == 0 || + std::strcmp(name, "ro.product.cpu.abilist") == 0 || + std::strcmp(name, "ro.product.cpu.abilist64") == 0) { + result = GuestAbiPropertyValue(); + } else if (std::strcmp(name, "ro.product.cpu.abilist32") == 0) { + result = ""; } else if (std::strcmp(name, "ro.product.manufacturer") == 0) { result = EnvironmentOr("MOCKTAIL_DEVICE_MANUFACTURER", "Mocktail"); } else if (std::strcmp(name, "ro.product.model") == 0) { diff --git a/src/compat/bionic_pthread_create_runtime.cc b/src/compat/bionic_pthread_create_runtime.cc index cda9e83..1ecacc2 100644 --- a/src/compat/bionic_pthread_create_runtime.cc +++ b/src/compat/bionic_pthread_create_runtime.cc @@ -1,10 +1,14 @@ #include "compat/bionic_pthread_create_runtime.h" +#include "compat/bionic_abi_exports.h" + #include #include +#include #include #include +#include #include #include @@ -69,24 +73,28 @@ size_t HostStackSizeForGuest(size_t guest_stack_size) noexcept { return guest_stack_size; } -int CopySupportedThreadAttributes(const pthread_attr_t& source, +// Guest pthread_attr_t buffers use the Bionic LP64 layout; they are never a +// host pthread_attr_t, so decode the fixed fields directly. +int GuestDetachState(const MocktailBionicPthreadAttr& source) noexcept { + return (source.flags & kMocktailBionicAttrFlagDetached) != 0 + ? PTHREAD_CREATE_DETACHED + : PTHREAD_CREATE_JOINABLE; +} + +size_t GuestStackSize(const MocktailBionicPthreadAttr& source) noexcept { + return source.stack_size != 0 ? source.stack_size + : kBionicLp64DefaultThreadStackSize; +} + +int CopySupportedThreadAttributes(const MocktailBionicPthreadAttr& source, pthread_attr_t* destination) noexcept { - int detach_state = PTHREAD_CREATE_JOINABLE; - int result = pthread_attr_getdetachstate(&source, &detach_state); - if (result != 0) { - return result; - } - result = pthread_attr_setdetachstate(destination, detach_state); + int result = + pthread_attr_setdetachstate(destination, GuestDetachState(source)); if (result != 0) { return result; } - size_t guard_size = 0; - result = pthread_attr_getguardsize(&source, &guard_size); - if (result != 0) { - return result; - } - result = pthread_attr_setguardsize(destination, guard_size); + result = pthread_attr_setguardsize(destination, source.guard_size); if (result != 0) { return result; } @@ -95,53 +103,38 @@ int CopySupportedThreadAttributes(const pthread_attr_t& source, // pthread_attr_setstack. Copying pthread_attr_getstack's address is unsafe: // glibc represents an automatically allocated stack with a synthetic // address derived from its size. - size_t stack_size = 0; - result = pthread_attr_getstacksize(&source, &stack_size); - if (result != 0) { - return result; - } - result = pthread_attr_setstacksize(destination, - HostStackSizeForGuest(stack_size)); + result = pthread_attr_setstacksize( + destination, HostStackSizeForGuest(GuestStackSize(source))); if (result != 0) { return result; } - int inherit_scheduler = PTHREAD_INHERIT_SCHED; - result = pthread_attr_getinheritsched(&source, &inherit_scheduler); - if (result != 0) { - return result; + // Bionic applies the attribute's scheduling policy at creation whenever it + // differs from the normal policy. + if (RejectRealtimeScheduling(source.sched_policy)) { + return EPERM; } - if (inherit_scheduler == PTHREAD_EXPLICIT_SCHED) { - int scheduler_policy = 0; - sched_param scheduler_parameters{}; - result = pthread_attr_getschedpolicy(&source, &scheduler_policy); - if (result == 0) { - result = pthread_attr_getschedparam(&source, &scheduler_parameters); - } - if (result == 0 && RejectRealtimeScheduling(scheduler_policy)) { - return EPERM; - } - if (result == 0) { - result = pthread_attr_setschedpolicy(destination, scheduler_policy); - } - if (result == 0) { - result = pthread_attr_setschedparam(destination, &scheduler_parameters); - } + if (source.sched_policy != SCHED_NORMAL) { + result = pthread_attr_setschedpolicy(destination, source.sched_policy); if (result != 0) { return result; } + sched_param scheduler_parameters{}; + scheduler_parameters.sched_priority = source.sched_priority; + result = pthread_attr_setschedparam(destination, &scheduler_parameters); + if (result != 0) { + return result; + } + return pthread_attr_setinheritsched(destination, PTHREAD_EXPLICIT_SCHED); } - return pthread_attr_setinheritsched(destination, inherit_scheduler); + + return pthread_attr_setinheritsched(destination, PTHREAD_INHERIT_SCHED); } -int CopyRequiredThreadAttributes(const pthread_attr_t& source, +int CopyRequiredThreadAttributes(const MocktailBionicPthreadAttr& source, pthread_attr_t* destination) noexcept { - int detach_state = PTHREAD_CREATE_JOINABLE; - int result = pthread_attr_getdetachstate(&source, &detach_state); - if (result != 0) { - return result; - } - result = pthread_attr_setdetachstate(destination, detach_state); + int result = + pthread_attr_setdetachstate(destination, GuestDetachState(source)); if (result != 0) { return result; } @@ -149,32 +142,20 @@ int CopyRequiredThreadAttributes(const pthread_attr_t& source, // The retry must retain an explicitly requested guest stack. Falling back // to a null/default host attr is unsafe on musl, whose default stack is much // smaller than Bionic's and can be exhausted by a single libroblox frame. - size_t stack_size = 0; - result = pthread_attr_getstacksize(&source, &stack_size); - if (result != 0) { - return result; - } - return pthread_attr_setstacksize(destination, - HostStackSizeForGuest(stack_size)); + return pthread_attr_setstacksize( + destination, HostStackSizeForGuest(GuestStackSize(source))); } -int ConfigureHostSafeStackFallback(const pthread_attr_t* source, +int ConfigureHostSafeStackFallback(const MocktailBionicPthreadAttr* source, pthread_attr_t* destination) noexcept { size_t guest_stack_size = 0; if (source != nullptr) { - int detach_state = PTHREAD_CREATE_JOINABLE; - int result = pthread_attr_getdetachstate(source, &detach_state); - if (result != 0) { - return result; - } - result = pthread_attr_setdetachstate(destination, detach_state); - if (result != 0) { - return result; - } - result = pthread_attr_getstacksize(source, &guest_stack_size); + int result = + pthread_attr_setdetachstate(destination, GuestDetachState(*source)); if (result != 0) { return result; } + guest_stack_size = GuestStackSize(*source); } size_t host_default_stack_size = 0; @@ -207,7 +188,8 @@ void ConfigureBionicPthreadThreadInitializer( g_thread_initializer.store(initializer, std::memory_order_release); } -int CreateBionicPthread(pthread_t* thread, const pthread_attr_t* attr, +int CreateBionicPthread(pthread_t* thread, + const MocktailBionicPthreadAttr* attr, void* (*start_routine)(void*), void* argument) { if (thread == nullptr || start_routine == nullptr) { return EINVAL; @@ -289,10 +271,123 @@ int CreateBionicPthread(pthread_t* thread, const pthread_attr_t* attr, } // namespace mocktail::compat -extern "C" int mocktail_bionic_pthread_create(pthread_t* thread, - const pthread_attr_t* attr, - void* (*start_routine)(void*), - void* argument) { +extern "C" { + +int mocktail_pthread_attr_init(MocktailBionicPthreadAttr* attr) { + if (attr == nullptr) { + return EINVAL; + } + attr->flags = 0; + attr->stack_base = nullptr; + attr->stack_size = + mocktail::compat::kBionicLp64DefaultThreadStackSize; + const long page_size = sysconf(_SC_PAGESIZE); + attr->guard_size = page_size > 0 ? static_cast(page_size) : 0; + attr->sched_policy = SCHED_OTHER; + attr->sched_priority = 0; + std::memset(attr->reserved, 0, sizeof(attr->reserved)); + return 0; +} + +int mocktail_pthread_attr_destroy(MocktailBionicPthreadAttr* attr) { + (void)attr; + return 0; +} + +int mocktail_pthread_attr_setstacksize(MocktailBionicPthreadAttr* attr, + size_t stack_size) { + if (attr == nullptr || stack_size < PTHREAD_STACK_MIN) { + return EINVAL; + } + attr->stack_size = stack_size; + attr->flags &= ~kMocktailBionicAttrFlagUserStack; + return 0; +} + +int mocktail_pthread_attr_setdetachstate(MocktailBionicPthreadAttr* attr, + int detach_state) { + if (attr == nullptr || (detach_state != PTHREAD_CREATE_DETACHED && + detach_state != PTHREAD_CREATE_JOINABLE)) { + return EINVAL; + } + if (detach_state == PTHREAD_CREATE_DETACHED) { + attr->flags |= kMocktailBionicAttrFlagDetached; + } else { + attr->flags &= ~kMocktailBionicAttrFlagDetached; + } + return 0; +} + +int mocktail_pthread_attr_setschedparam(MocktailBionicPthreadAttr* attr, + const struct sched_param* parameters) { + if (attr == nullptr || parameters == nullptr) { + return EINVAL; + } + attr->sched_priority = parameters->sched_priority; + return 0; +} + +int mocktail_pthread_getattr_np(MocktailBionicPthreadAttr* attr) { + if (attr == nullptr) { + return EINVAL; + } + pthread_attr_t host_attr; + int result = pthread_getattr_np(pthread_self(), &host_attr); + if (result != 0) { + return result; + } + void* stack_base = nullptr; + size_t stack_size = 0; + result = pthread_attr_getstack(&host_attr, &stack_base, &stack_size); + size_t guard_size = 0; + if (result == 0) { + result = pthread_attr_getguardsize(&host_attr, &guard_size); + } + int detach_state = PTHREAD_CREATE_JOINABLE; + if (result == 0) { + result = pthread_attr_getdetachstate(&host_attr, &detach_state); + } + sched_param parameters{}; + int policy = SCHED_OTHER; + if (result == 0) { + result = pthread_getschedparam(pthread_self(), &policy, ¶meters); + } + pthread_attr_destroy(&host_attr); + if (result != 0) { + return result; + } + attr->flags = detach_state == PTHREAD_CREATE_DETACHED + ? kMocktailBionicAttrFlagDetached + : 0; + attr->stack_base = stack_base; + attr->stack_size = stack_size; + attr->guard_size = guard_size; + attr->sched_policy = policy; + attr->sched_priority = parameters.sched_priority; + std::memset(attr->reserved, 0, sizeof(attr->reserved)); + return 0; +} + +int mocktail_pthread_attr_getstack(const MocktailBionicPthreadAttr* attr, + void** stack_base, size_t* stack_size) { + if (attr == nullptr) { + return EINVAL; + } + if (stack_base != nullptr) { + *stack_base = attr->stack_base; + } + if (stack_size != nullptr) { + *stack_size = attr->stack_size; + } + return 0; +} + + +} // extern "C" + +extern "C" int mocktail_bionic_pthread_create( + pthread_t* thread, const MocktailBionicPthreadAttr* attr, + void* (*start_routine)(void*), void* argument) { return mocktail::compat::CreateBionicPthread(thread, attr, start_routine, argument); } diff --git a/src/compat/host_abi_experiment.cc b/src/compat/host_abi_experiment.cc index 6779948..c4bda71 100644 --- a/src/compat/host_abi_experiment.cc +++ b/src/compat/host_abi_experiment.cc @@ -39,12 +39,23 @@ bool PatchAbsoluteJump(uintptr_t address, void *target) noexcept { return false; } +#if defined(__aarch64__) + // ldr x16, #8 ; br x16 ; .quad target + // x16 is the intra-procedure-call scratch register, free to clobber. + unsigned char patch[] = { + 0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6, + 0, 0, 0, 0, 0, 0, 0, 0, + }; + const uintptr_t target_address = reinterpret_cast(target); + std::memcpy(patch + 8, &target_address, sizeof(target_address)); +#else unsigned char patch[] = { 0x48, 0xb8, // movabs rax, imm64 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0, // jmp rax }; const uintptr_t target_address = reinterpret_cast(target); std::memcpy(patch + 2, &target_address, sizeof(target_address)); +#endif const uintptr_t page_mask = static_cast(page_size) - 1; const uintptr_t page = address & ~page_mask; diff --git a/src/compat/host_abi_profile.cc b/src/compat/host_abi_profile.cc index a769381..fa5d9a4 100644 --- a/src/compat/host_abi_profile.cc +++ b/src/compat/host_abi_profile.cc @@ -6,6 +6,34 @@ namespace mocktail::compat { namespace { constexpr HostAbiProfile kProfiles[] = { + // Roblox 2.736.1408 arm64-v8a. Every RVA was derived from the AArch64 + // payload of the same release as the x86-64 reference above, matched by + // allocator internals, init_array call graphs, and static state layout. + // Constructor policy mirrors x86-64 by semantic role: the pre-init + // entries 0..3 stay skipped, index 4 is mi_process_load (thread init + // checkpoint), index 5 is allocator wiring and index 6 is the lazy clock + // singleton getter, both excluded under host bridges only. + {"ed2b035768b1513561d0e6ea7f9bbec35b0e60e5", + {{{0x1d53840, HostBridgeKind::kAllocate, "small-allocate"}, + {0x1d56c34, HostBridgeKind::kUsableSize, "usable-size"}, + {0x1d6ef80, HostBridgeKind::kReallocate, "reallocate"}, + {0x1d53568, HostBridgeKind::kAllocate, "allocate"}, + {0x61d3850, HostBridgeKind::kAlignedAllocate, + "aligned-allocate-direct"}, + {0x1d57328, HostBridgeKind::kFree, "free"}}}, + 6, + {0, 0, 0x7296c88, 0x400, 0x1d53c54, 0x1dc76a0, + 0x71d6c90, 0x71d2c40, 0x400000}, + {0x1d53568, 0x1d57328}, + 0x6733128, + 3565, + {{{4, 5}, {7, 3565}}}, + 2, + {{{4, 3565}}}, + 1, + 4, + {0x2132c5c, 0x71d6d30}, + HostAllocatorStrategy::kNativeMimalloc}, // Roblox 2.736.1408. Derived from the verified 2908 profile and validated // by two isolated no-recovery Vulkan canaries and real game sessions. {"ade08266c67aee88ec9c1d00902150e1684dad3a", diff --git a/src/compat/host_abi_profile_loader.cc b/src/compat/host_abi_profile_loader.cc index 2c3c1a2..2467ab5 100644 --- a/src/compat/host_abi_profile_loader.cc +++ b/src/compat/host_abi_profile_loader.cc @@ -32,6 +32,7 @@ #include #include "compat/elf_build_id.h" +#include "compat/guest_abi.h" namespace mocktail::compat { namespace { @@ -611,19 +612,26 @@ std::unique_ptr ParseProfile(const Json& root, *error = "external host ABI profile has inconsistent constructor policy"; return nullptr; } - if (owned->profile.init_array_count <= 5 || + // The verified constructor layout is the same on both guest ABIs: a single + // process-load constructor at the thread-initializer checkpoint, two wiring + // entries excluded under host bridges, and one contiguous native-mimalloc + // replay from the checkpoint to the end of .init_array. Only the checkpoint + // index itself differs between payloads. + const std::size_t checkpoint = + owned->profile.native_mimalloc_thread_initializer_after_constructor; + if (owned->profile.init_array_count <= checkpoint + 3 || owned->profile.constructor_run_range_count != 2 || - owned->profile.constructor_run_ranges[0].begin != 2 || - owned->profile.constructor_run_ranges[0].end_exclusive != 3 || - owned->profile.constructor_run_ranges[1].begin != 5 || + owned->profile.constructor_run_ranges[0].begin != checkpoint || + owned->profile.constructor_run_ranges[0].end_exclusive != checkpoint + 1 || + owned->profile.constructor_run_ranges[1].begin != checkpoint + 3 || owned->profile.constructor_run_ranges[1].end_exclusive != owned->profile.init_array_count || owned->profile.native_mimalloc_constructor_run_range_count != 1 || - owned->profile.native_mimalloc_constructor_run_ranges[0].begin != 2 || + owned->profile.native_mimalloc_constructor_run_ranges[0].begin != + checkpoint || owned->profile.native_mimalloc_constructor_run_ranges[0].end_exclusive != owned->profile.init_array_count || - owned->profile.native_mimalloc_thread_initializer_after_constructor != - 2) { + (kGuestElfMachine == EM_X86_64 && checkpoint != 2)) { *error = "external host ABI profile changed the verified constructor policy"; return nullptr; @@ -669,9 +677,10 @@ bool ValidateProfileAgainstElf(const std::string& payload_path, } GElf_Ehdr header{}; if (gelf_getehdr(elf.get(), &header) == nullptr || - gelf_getclass(elf.get()) != ELFCLASS64 || header.e_machine != EM_X86_64 || - header.e_type != ET_DYN) { - *error = "payload must be an x86-64 ET_DYN ELF object"; + gelf_getclass(elf.get()) != ELFCLASS64 || + header.e_machine != kGuestElfMachine || header.e_type != ET_DYN) { + *error = "payload must be a " + std::string(kGuestAbi) + + " ET_DYN ELF object"; return false; } diff --git a/src/legacy/legacy_runtime.cc b/src/legacy/legacy_runtime.cc index 7002d02..85a06d3 100644 --- a/src/legacy/legacy_runtime.cc +++ b/src/legacy/legacy_runtime.cc @@ -36,7 +36,9 @@ #include #include #include +#if defined(__x86_64__) #include +#endif #include #include @@ -49,6 +51,7 @@ #include "compat/bionic_socket_runtime.h" #include "compat/build_profile.h" #include "compat/elf_build_id.h" +#include "compat/guest_abi.h" #include "compat/host_abi_experiment.h" #include "compat/host_abi_profile.h" #include "compat/host_allocator_bridge.h" @@ -1006,7 +1009,7 @@ struct BionicAddrInfo { }; static_assert(sizeof(BionicAddrInfo) == 48, - "unexpected x86_64 bionic addrinfo size"); + "unexpected bionic addrinfo size"); bool DnsTraceEnabled() { return IsEnabled("MOCKTAIL_DNS_TRACE"); @@ -1371,15 +1374,31 @@ bool IsUnsafeSoftTimeoutModule(void* rip) { void JniOnLoadTimeoutAlarm(int, siginfo_t* info, void* context) { static_cast(info); auto* uc = static_cast(context); +#if defined(__x86_64__) auto rip = uc ? static_cast(uc->uc_mcontext.gregs[REG_RIP]) : 0; auto rsp = uc ? static_cast(uc->uc_mcontext.gregs[REG_RSP]) : 0; auto rbp = uc ? static_cast(uc->uc_mcontext.gregs[REG_RBP]) : 0; auto rax = uc ? static_cast(uc->uc_mcontext.gregs[REG_RAX]) : 0; + const char* regs_format = + " [timeout] RIP=0x%016llx RSP=0x%016llx RBP=0x%016llx " + "RAX=0x%016llx\n"; +#elif defined(__aarch64__) + auto rip = uc ? static_cast(uc->uc_mcontext.pc) : 0; + auto rsp = uc ? static_cast(uc->uc_mcontext.sp) : 0; + auto rbp = uc ? static_cast(uc->uc_mcontext.regs[29]) : 0; + auto rax = uc ? static_cast(uc->uc_mcontext.regs[0]) : 0; + const char* regs_format = + " [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx " + "X0=0x%016llx\n"; +#else + uintptr_t rip = 0, rsp = 0, rbp = 0, rax = 0; + const char* regs_format = + " [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx " + "X0=0x%016llx\n"; +#endif char regs_msg[192]; int len = std::snprintf( - regs_msg, sizeof(regs_msg), - " [timeout] RIP=0x%016llx RSP=0x%016llx RBP=0x%016llx " - "RAX=0x%016llx\n", + regs_msg, sizeof(regs_msg), regs_format, static_cast(rip), static_cast(rsp), static_cast(rbp), static_cast(rax)); write(2, regs_msg, static_cast(len)); @@ -2422,10 +2441,12 @@ jobject BuildDeviceParams(JNIEnv* env) { GetEnvString("MOCKTAIL_DEVICE_NAME", "Mocktail Linux"); const std::string manufacturer = GetEnvString("MOCKTAIL_DEVICE_MANUFACTURER", "Mocktail"); - const std::string device_sku = - GetEnvString("MOCKTAIL_DEVICE_SKU", "mocktail-x86_64"); + const std::string device_sku = GetEnvString( + "MOCKTAIL_DEVICE_SKU", + ("mocktail-" + std::string(mocktail::compat::kGuestCpuName)).c_str()); const std::string soc_model = - GetEnvString("MOCKTAIL_DEVICE_SOC_MODEL", "x86_64"); + GetEnvString("MOCKTAIL_DEVICE_SOC_MODEL", + std::string(mocktail::compat::kGuestCpuName).c_str()); SetStringField(env, params, "osVersion", "33"); SetStringField(env, params, "deviceName", device_name.c_str()); const std::string app_version = @@ -4621,12 +4642,14 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options, // Roblox internal threads trigger SI_KERNEL traps from CET shadow-stack // return mismatches. Unsupported kernels ignore this request. +#if defined(__x86_64__) { long r = syscall(SYS_arch_prctl, ARCH_SHSTK_DISABLE, ARCH_SHSTK_SHSTK); if (r == 0) { std::cout << " [cet] shadow-stack (SHSTK) disabled\n"; } } +#endif if (build_profile.allow_legacy_binary_patches) { std::cerr @@ -5545,6 +5568,27 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options, reinterpret_cast(mocktail_pthread_spin_trylock)); linker::RegisterSymbol("pthread_spin_unlock", reinterpret_cast(mocktail_pthread_spin_unlock)); + linker::RegisterSymbol( + "pthread_attr_init", + reinterpret_cast(mocktail_pthread_attr_init)); + linker::RegisterSymbol( + "pthread_attr_destroy", + reinterpret_cast(mocktail_pthread_attr_destroy)); + linker::RegisterSymbol( + "pthread_attr_setstacksize", + reinterpret_cast(mocktail_pthread_attr_setstacksize)); + linker::RegisterSymbol( + "pthread_attr_setdetachstate", + reinterpret_cast(mocktail_pthread_attr_setdetachstate)); + linker::RegisterSymbol( + "pthread_attr_setschedparam", + reinterpret_cast(mocktail_pthread_attr_setschedparam)); + linker::RegisterSymbol( + "pthread_getattr_np", + reinterpret_cast(mocktail_pthread_getattr_np)); + linker::RegisterSymbol( + "pthread_attr_getstack", + reinterpret_cast(mocktail_pthread_attr_getstack)); linker::RegisterSymbol("pthread_barrier_init", reinterpret_cast(mocktail_pthread_barrier_init)); linker::RegisterSymbol( @@ -5664,7 +5708,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options, if (roblox_handle == nullptr) { std::cerr << "\n[FATAL] Could not load '" << library_path << "'.\n" - << " Extract lib/x86_64/libroblox.so from a Roblox APK and\n" + << " Extract lib/" << compat::kGuestAbi + << "/libroblox.so from a Roblox APK and\n" << " place it at the path above (or set ROBLOX_LIB_PATH).\n"; return EXIT_FAILURE; } diff --git a/src/runtime/command_line.cc b/src/runtime/command_line.cc index 99f8670..6e1c492 100644 --- a/src/runtime/command_line.cc +++ b/src/runtime/command_line.cc @@ -5,6 +5,7 @@ #include #include +#include "compat/guest_abi.h" #include "runtime/roblox_launch_uri.h" namespace mocktail { @@ -241,11 +242,12 @@ std::string CommandLineUsage(const std::string& program_name) { usage << "Usage: " << (program_name.empty() ? "mocktail" : program_name) << " [options] [roblox-uri]\n\n" - << "Normal startup downloads and activates a verified managed x86_64 " - "Roblox payload automatically.\n\n" + << "Normal startup downloads and activates a verified managed " + << compat::kGuestAbi + << " Roblox payload automatically.\n\n" << "Options:\n" << " --roblox-lib Development override for a specific " - "x86_64 libroblox.so\n" + << compat::kGuestAbi << " libroblox.so\n" << " --headless Run without creating an SDL window\n" << " --windowed Force windowed startup (default)\n" << " --graphics direct-vulkan | opengl | system | " diff --git a/src/runtime/graphics_launch_policy.cc b/src/runtime/graphics_launch_policy.cc index 14dd8b7..20c91de 100644 --- a/src/runtime/graphics_launch_policy.cc +++ b/src/runtime/graphics_launch_policy.cc @@ -27,12 +27,21 @@ constexpr const char* kIcdDirectories[] = { "/usr/local/share/vulkan/icd.d", }; +#if defined(__aarch64__) +constexpr char kNativeArchitecture[] = "aarch64"; + +constexpr const char* kForeignArchitectures[] = { + "i686", "i586", "i486", "i386", "x86_64", "x86.", + "amd64", "armhf", "armv7", "ppc64", "riscv64", "s390x", +}; +#else constexpr char kNativeArchitecture[] = "x86_64"; constexpr const char* kForeignArchitectures[] = { "i686", "i586", "i486", "i386", "x86.", "aarch64", "arm64", "armhf", "armv7", "ppc64", "riscv64", "s390x", }; +#endif struct HostGpus { bool intel = false; diff --git a/src/runtime/runtime_paths.cc b/src/runtime/runtime_paths.cc index 8a8ffe8..46ea8fc 100644 --- a/src/runtime/runtime_paths.cc +++ b/src/runtime/runtime_paths.cc @@ -14,6 +14,8 @@ #include #include +#include "compat/guest_abi.h" + namespace mocktail { namespace runtime { namespace { @@ -438,8 +440,8 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const { const std::filesystem::path library = canonical_root / "libroblox.so"; const std::filesystem::path package_root = canonical_root / "sober_apk"; const std::filesystem::path base_apk = package_root / "base.apk"; - const std::filesystem::path x86_64_split_apk = - package_root / "split_config.x86_64.apk"; + const std::filesystem::path arch_split_apk = + package_root / compat::kGuestSplitApkFile; const std::filesystem::path assets_root = canonical_root / "assets"; const std::filesystem::path assets = canonical_root / "assets/content"; const std::filesystem::file_status library_status = @@ -460,7 +462,8 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const { for (const auto& [path, name] : std::array, 2>{ std::pair{base_apk, "base.apk"}, - std::pair{x86_64_split_apk, "split_config.x86_64.apk"}}) { + std::pair{arch_split_apk, + compat::kGuestSplitApkFile.data()}}) { const std::filesystem::file_status status = std::filesystem::symlink_status(path, filesystem_error); if (filesystem_error || !std::filesystem::is_regular_file(status) || @@ -489,7 +492,7 @@ ActivePayloadPaths RuntimePaths::ResolveActivePayload() const { result.root = canonical_root; result.roblox_library = library; result.base_apk = base_apk; - result.x86_64_split_apk = x86_64_split_apk; + result.arch_split_apk = arch_split_apk; result.assets_content = assets; const auto compatibility_manifest = diff --git a/src/update/apk_bundle.cc b/src/update/apk_bundle.cc index c7f9148..2db5328 100644 --- a/src/update/apk_bundle.cc +++ b/src/update/apk_bundle.cc @@ -28,6 +28,7 @@ #include #include "compat/elf_build_id.h" +#include "compat/guest_abi.h" #include "update/android_manifest.h" #include "update/apk_signature.h" #include "update/payload_integrity.h" @@ -173,10 +174,15 @@ bool EndsWith(std::string_view value, std::string_view suffix) { struct ApkCandidate { std::filesystem::path path; AndroidManifestIdentity identity; - bool has_x86_64_library = false; + bool has_guest_library = false; bool has_assets = false; }; +std::string GuestLibEntryName() { + return std::string(compat::kGuestLibApkEntryPrefix) + + std::string(compat::kGuestAbi) + "/libroblox.so"; +} + bool CollectCandidates(const std::vector& archives, const std::filesystem::path& directory, std::vector* candidates, @@ -250,7 +256,7 @@ bool InspectCandidates(const std::vector& paths, ApkCandidate candidate; candidate.path = path; candidate.identity = std::move(identity); - candidate.has_x86_64_library = HasEntry(list, "lib/x86_64/libroblox.so"); + candidate.has_guest_library = HasEntry(list, GuestLibEntryName()); candidate.has_assets = std::any_of( list.entries.begin(), list.entries.end(), [](const ZipEntry& entry) { return entry.name.size() > 7 && entry.name.substr(0, 7) == "assets/"; @@ -314,7 +320,8 @@ std::vector CommonTrustedCertificates( const ApkSignatureResult split_signature = base == split ? base_signature : VerifyApkSignature(split); if (!split_signature) { - *error = "x86_64 APK signature: " + split_signature.error; + *error = std::string(compat::kGuestAbi) + + " APK signature: " + split_signature.error; return {}; } std::vector accepted; @@ -356,9 +363,11 @@ bool ValidateElf(const std::filesystem::path& path, std::string* build_id, } GElf_Ehdr header{}; if (gelf_getehdr(elf.get(), &header) == nullptr || - header.e_machine != EM_X86_64 || gelf_getclass(elf.get()) != ELFCLASS64) { + header.e_machine != compat::kGuestElfMachine || + gelf_getclass(elf.get()) != ELFCLASS64) { close_elf(); - *error = "libroblox.so is not an x86-64 ELF"; + *error = "libroblox.so is not a " + std::string(compat::kGuestAbi) + + " ELF"; return false; } std::size_t section_count = 0; @@ -464,16 +473,18 @@ PreparedPayload PreparePayloadFromArchives( base = &candidate; } } + const std::string guest_split_name(compat::kGuestApkSplitName); for (const ApkCandidate& candidate : candidates) { - if (candidate.has_x86_64_library && - (candidate.identity.split_name == "config.x86_64" || + if (candidate.has_guest_library && + (candidate.identity.split_name == guest_split_name || candidate.identity.split_name.empty()) && split == nullptr) { split = &candidate; } } if (base == nullptr || split == nullptr) { - result.error = "no matching Roblox base and x86_64 APK pair was found"; + result.error = "no matching Roblox base and " + + std::string(compat::kGuestAbi) + " APK pair was found"; return result; } const std::set trusted = @@ -488,12 +499,13 @@ PreparedPayload PreparePayloadFromArchives( if (filesystem_error || !CopyRegular(base->path, prepared / "sober_apk/base.apk", &result.error) || - !CopyRegular(split->path, prepared / "sober_apk/split_config.x86_64.apk", + !CopyRegular(split->path, + prepared / "sober_apk" / compat::kGuestSplitApkFile, &result.error)) { if (result.error.empty()) result.error = "cannot create prepared payload"; return result; } - if (!ExtractZipEntry(split->path, "lib/x86_64/libroblox.so", + if (!ExtractZipEntry(split->path, GuestLibEntryName(), prepared / "libroblox.so", kMaximumApkBytes, &result.error)) { return result; @@ -527,7 +539,7 @@ PreparedPayload PreparePayloadFromArchives( const std::string base_hash = HashRegularFile(prepared / "sober_apk/base.apk", &hash_error); const std::string split_hash = HashRegularFile( - prepared / "sober_apk/split_config.x86_64.apk", &hash_error); + prepared / "sober_apk" / compat::kGuestSplitApkFile, &hash_error); if (!hash_error.empty()) { result.error = hash_error; return result; @@ -537,12 +549,12 @@ PreparedPayload PreparePayloadFromArchives( {"package", "com.roblox.client"}, {"version_name", expected.version_name}, {"version_code", expected.version_code}, - {"abi", "x86_64"}, + {"abi", std::string(compat::kGuestAbi)}, {"elf_build_id", build_id}, {"sha256", {{"libroblox", library_hash}, {"base_apk", base_hash}, - {"x86_64_split_apk", split_hash}}}, + {std::string(compat::kGuestSplitApkHashKey), split_hash}}}, {"signing_certificates_sha256", accepted}, {"assets", {{"file_count", asset_count_verified}, diff --git a/src/update/apkpure_provider.cc b/src/update/apkpure_provider.cc index ca55c50..d8bd690 100644 --- a/src/update/apkpure_provider.cc +++ b/src/update/apkpure_provider.cc @@ -9,6 +9,7 @@ #include #include +#include "compat/guest_abi.h" #include "update/http_download.h" namespace mocktail::update { @@ -199,7 +200,8 @@ std::vector ParseApkPureExactDownloadUrls( } ProviderVersion ApkPureProvider::CheckLatest() const { - const HttpBytesResult metadata = FetchMetadata("x86_64"); + const HttpBytesResult metadata = + FetchMetadata(std::string(compat::kGuestAbi)); if (!metadata) return ProviderVersion{{}, 0, metadata.error}; return ParseApkPureLatestMetadata(metadata.bytes); } @@ -223,7 +225,7 @@ ProviderDownloadResult ApkPureProvider::DownloadExact( result.error = "APKPure output directory must exist and be empty"; return result; } - HttpBytesResult metadata = FetchMetadata("x86_64"); + HttpBytesResult metadata = FetchMetadata(std::string(compat::kGuestAbi)); if (!metadata) { result.error = metadata.error; return result; diff --git a/src/update/candidate_approval.cc b/src/update/candidate_approval.cc index 49c32d8..cd18837 100644 --- a/src/update/candidate_approval.cc +++ b/src/update/candidate_approval.cc @@ -19,6 +19,7 @@ #include #include "compat/elf_build_id.h" +#include "compat/guest_abi.h" #include "update/payload_integrity.h" namespace mocktail::update { @@ -260,7 +261,7 @@ std::string PayloadRuntimeFingerprint( evidence.append("base_apk=") .append(payload.metadata.base_apk_sha256) .append("\n"); - evidence.append("x86_64_split=") + evidence.append(std::string(compat::kGuestAbi) + "_split=") .append(payload.metadata.split_apk_sha256) .append("\n"); evidence.append("assets=") diff --git a/src/update/host_abi_deriver.cc b/src/update/host_abi_deriver.cc index 0222161..2134c96 100644 --- a/src/update/host_abi_deriver.cc +++ b/src/update/host_abi_deriver.cc @@ -28,6 +28,7 @@ #include "compat/elf_build_id.h" #include "compat/fmod_output_device_contract.h" +#include "compat/guest_abi.h" #include "update/payload_integrity.h" namespace mocktail::update { @@ -220,9 +221,10 @@ class ElfImage final { if (elf_ == nullptr || elf_kind(elf_) != ELF_K_ELF || gelf_getehdr(elf_, &header) == nullptr || gelf_getclass(elf_) != ELFCLASS64 || header.e_type != ET_DYN || - header.e_machine != EM_X86_64 || header.e_version != EV_CURRENT || + header.e_machine != compat::kGuestElfMachine || + header.e_version != EV_CURRENT || header.e_ident[EI_DATA] != ELFDATA2LSB) { - *error = "candidate must be a little-endian x86-64 ET_DYN ELF"; + *error = "candidate must be a little-endian guest-abi ET_DYN ELF"; return false; } std::size_t program_count = 0; @@ -1879,7 +1881,7 @@ std::optional LoadCandidateMetadata(const std::filesystem::path& root, metadata->contains("sha256") ? &(*metadata)["sha256"] : nullptr; if (metadata->value("schema_version", 0) != 1 || metadata->value("package", "") != "com.roblox.client" || - metadata->value("abi", "") != "x86_64" || version_name.empty() || + metadata->value("abi", "") != compat::kGuestAbi || version_name.empty() || version_code == 0 || metadata->value("elf_build_id", "") != candidate.build_id() || hashes == nullptr || !hashes->is_object() || diff --git a/src/update/payload_integrity.cc b/src/update/payload_integrity.cc index 1c12c6a..488bc82 100644 --- a/src/update/payload_integrity.cc +++ b/src/update/payload_integrity.cc @@ -18,6 +18,8 @@ #include #include +#include "compat/guest_abi.h" + namespace mocktail::update { namespace { @@ -291,7 +293,7 @@ PayloadIntegrityResult InspectPreparedPayload( for (const std::filesystem::path& relative : { std::filesystem::path("libroblox.so"), std::filesystem::path("sober_apk/base.apk"), - std::filesystem::path("sober_apk/split_config.x86_64.apk"), + std::filesystem::path("sober_apk") / compat::kGuestSplitApkFile, std::filesystem::path("roblox_payload.json"), }) { const auto status = @@ -335,7 +337,7 @@ PayloadIntegrityResult InspectPreparedPayload( result.metadata.library_sha256 = document["sha256"].value("libroblox", ""); result.metadata.base_apk_sha256 = document["sha256"].value("base_apk", ""); result.metadata.split_apk_sha256 = - document["sha256"].value("x86_64_split_apk", ""); + document["sha256"].value(std::string(compat::kGuestSplitApkHashKey), ""); if (!document["assets"].contains("file_count") || !document["assets"]["file_count"].is_number_unsigned()) { result.error = "payload metadata asset count is invalid"; @@ -368,7 +370,8 @@ PayloadIntegrityResult VerifyPreparedPayload( const std::array, 3> files = {{ {"libroblox.so", result.metadata.library_sha256}, {"sober_apk/base.apk", result.metadata.base_apk_sha256}, - {"sober_apk/split_config.x86_64.apk", result.metadata.split_apk_sha256}, + {std::filesystem::path("sober_apk") / compat::kGuestSplitApkFile, + result.metadata.split_apk_sha256}, }}; for (const auto& [relative, expected] : files) { const std::string actual = diff --git a/tests/bionic_abi_exports_test.cc b/tests/bionic_abi_exports_test.cc index 64e9560..0c4693b 100644 --- a/tests/bionic_abi_exports_test.cc +++ b/tests/bionic_abi_exports_test.cc @@ -1,5 +1,7 @@ #include "compat/bionic_abi_exports.h" +#include "compat/guest_abi.h" + #include #include @@ -37,8 +39,10 @@ TEST(BionicAbiExportsTest, ReportsStableAndroidSystemProperties) { EXPECT_STREQ("35", value.data()); value.fill('\0'); - EXPECT_EQ(6, __system_property_get("ro.product.cpu.abi", value.data())); - EXPECT_STREQ("x86_64", value.data()); + EXPECT_EQ(static_cast(mocktail::compat::kGuestAbi.size()), + __system_property_get("ro.product.cpu.abi", value.data())); + EXPECT_STREQ(std::string(mocktail::compat::kGuestAbi).c_str(), + value.data()); value.fill('x'); EXPECT_EQ(0, __system_property_get("mocktail.unknown", value.data())); diff --git a/tests/bionic_pthread_create_runtime_test.cc b/tests/bionic_pthread_create_runtime_test.cc index 22fa8dd..d83bfc9 100644 --- a/tests/bionic_pthread_create_runtime_test.cc +++ b/tests/bionic_pthread_create_runtime_test.cc @@ -5,6 +5,8 @@ #include #include +#include "compat/bionic_abi_exports.h" + #include #include @@ -189,9 +191,10 @@ TEST(BionicPthreadCreateRuntimeTest, GrowsRejectedExplicitGuestStack) { 0); ASSERT_EQ(pthread_attr_destroy(&host_defaults), 0); - pthread_attr_t guest_attributes; - ASSERT_EQ(pthread_attr_init(&guest_attributes), 0); - ASSERT_EQ(pthread_attr_setstacksize(&guest_attributes, kRequestedStackSize), + MocktailBionicPthreadAttr guest_attributes; + ASSERT_EQ(mocktail_pthread_attr_init(&guest_attributes), 0); + ASSERT_EQ(mocktail_pthread_attr_setstacksize(&guest_attributes, + kRequestedStackSize), 0); g_intercepted_create_calls.store(0, std::memory_order_release); @@ -206,7 +209,7 @@ TEST(BionicPthreadCreateRuntimeTest, GrowsRejectedExplicitGuestStack) { &thread, &guest_attributes, RecordThreadStackSize, nullptr); g_intercept_pthread_create.store(false, std::memory_order_release); - ASSERT_EQ(pthread_attr_destroy(&guest_attributes), 0); + ASSERT_EQ(mocktail_pthread_attr_destroy(&guest_attributes), 0); ASSERT_EQ(result, 0); ASSERT_EQ(pthread_join(thread, nullptr), 0); EXPECT_EQ(g_intercepted_create_calls.load(std::memory_order_acquire), 3); @@ -223,15 +226,16 @@ TEST(BionicPthreadCreateRuntimeTest, PreservesRequestedStackSize) { constexpr size_t kRequestedStackSize = 2 * 1024 * 1024; g_observed_stack_size.store(0, std::memory_order_release); - pthread_attr_t attributes; - ASSERT_EQ(pthread_attr_init(&attributes), 0); - ASSERT_EQ(pthread_attr_setstacksize(&attributes, kRequestedStackSize), 0); + MocktailBionicPthreadAttr attributes; + ASSERT_EQ(mocktail_pthread_attr_init(&attributes), 0); + ASSERT_EQ( + mocktail_pthread_attr_setstacksize(&attributes, kRequestedStackSize), 0); pthread_t thread{}; ASSERT_EQ(mocktail_bionic_pthread_create(&thread, &attributes, RecordThreadStackSize, nullptr), 0); - EXPECT_EQ(pthread_attr_destroy(&attributes), 0); + EXPECT_EQ(mocktail_pthread_attr_destroy(&attributes), 0); ASSERT_EQ(pthread_join(thread, nullptr), 0); ExpectRequestedStackSize( g_observed_stack_size.load(std::memory_order_acquire), @@ -240,13 +244,13 @@ TEST(BionicPthreadCreateRuntimeTest, PreservesRequestedStackSize) { TEST(BionicPthreadCreateRuntimeTest, RejectsExplicitRealtimeBeforeHostCreate) { for (const int policy : {SCHED_FIFO, SCHED_RR}) { - pthread_attr_t attributes; - ASSERT_EQ(pthread_attr_init(&attributes), 0); - ASSERT_EQ(pthread_attr_setinheritsched(&attributes, PTHREAD_EXPLICIT_SCHED), 0); - ASSERT_EQ(pthread_attr_setschedpolicy(&attributes, policy), 0); + MocktailBionicPthreadAttr attributes; + ASSERT_EQ(mocktail_pthread_attr_init(&attributes), 0); + attributes.sched_policy = policy; sched_param parameters{}; parameters.sched_priority = 99; - ASSERT_EQ(pthread_attr_setschedparam(&attributes, ¶meters), 0); + ASSERT_EQ( + mocktail_pthread_attr_setschedparam(&attributes, ¶meters), 0); g_intercepted_create_calls.store(0); g_intercept_pthread_create.store(true); pthread_t thread{}; @@ -255,7 +259,7 @@ TEST(BionicPthreadCreateRuntimeTest, RejectsExplicitRealtimeBeforeHostCreate) { g_intercept_pthread_create.store(false); EXPECT_EQ(result, EPERM); EXPECT_EQ(g_intercepted_create_calls.load(), 0); - EXPECT_EQ(pthread_attr_destroy(&attributes), 0); + EXPECT_EQ(mocktail_pthread_attr_destroy(&attributes), 0); } } diff --git a/tests/command_line_test.cc b/tests/command_line_test.cc index 75fdfe9..68cad80 100644 --- a/tests/command_line_test.cc +++ b/tests/command_line_test.cc @@ -2,6 +2,8 @@ #include +#include "compat/guest_abi.h" + #include #include #include @@ -274,8 +276,10 @@ TEST(CommandLineTest, UsageContainsEverySupportedOption) { EXPECT_EQ(usage.find("--login"), std::string::npos); EXPECT_EQ(usage.find("--import-cookie"), std::string::npos); EXPECT_NE(usage.find("native sign-in flow"), std::string::npos); - EXPECT_NE(usage.find("verified managed x86_64 Roblox payload"), - std::string::npos); + EXPECT_NE( + usage.find("verified managed " + + std::string(mocktail::compat::kGuestAbi) + " Roblox payload"), + std::string::npos); EXPECT_EQ(usage.find("Normal startup uses rbx_bin/libroblox.so"), std::string::npos); EXPECT_EQ(usage.find("--launch-request-json"), std::string::npos); diff --git a/tests/native_update_test.cc b/tests/native_update_test.cc index bf6a4a4..a316b7e 100644 --- a/tests/native_update_test.cc +++ b/tests/native_update_test.cc @@ -13,6 +13,7 @@ #include #include "compat/build_profile.h" +#include "compat/guest_abi.h" #include "compat/host_abi_profile.h" #include "update/apkpure_provider.h" #include "update/compatibility_catalog.h" @@ -643,7 +644,7 @@ TEST(PayloadStoreTest, RestagingIdenticalBytesKeepsTheInstalledPayload) { const std::filesystem::path restaged = temporary.root() / "restaged"; Write(prepared / "libroblox.so", "native-library"); Write(prepared / "sober_apk/base.apk", "base-apk"); - Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk"); + Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; @@ -659,8 +660,8 @@ TEST(PayloadStoreTest, RestagingIdenticalBytesKeepsTheInstalledPayload) { {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")}, - {"x86_64_split_apk", - HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}}, + {std::string(compat::kGuestSplitApkHashKey), + HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}}, {"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}}, {"source", "apk-pure-native"}, {"imported_at", "2026-09-04T03:49:16Z"}, @@ -704,7 +705,7 @@ TEST(PayloadStoreTest, StagesAndPromotesVerifiedExactPayload) { const std::filesystem::path prepared = temporary.root() / "prepared"; Write(prepared / "libroblox.so", "native-library"); Write(prepared / "sober_apk/base.apk", "base-apk"); - Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk"); + Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; @@ -722,8 +723,8 @@ TEST(PayloadStoreTest, StagesAndPromotesVerifiedExactPayload) { {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")}, - {"x86_64_split_apk", - HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}}, + {std::string(compat::kGuestSplitApkHashKey), + HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}}, {"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}}, }; Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n"); @@ -767,7 +768,7 @@ TEST(PayloadStoreTest, RestagesReadOnlyCorruptPayloadCollision) { const std::filesystem::path prepared = temporary.root() / "prepared"; Write(prepared / "libroblox.so", "native-library"); Write(prepared / "sober_apk/base.apk", "base-apk"); - Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk"); + Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; @@ -785,8 +786,8 @@ TEST(PayloadStoreTest, RestagesReadOnlyCorruptPayloadCollision) { {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")}, - {"x86_64_split_apk", - HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}}, + {std::string(compat::kGuestSplitApkHashKey), + HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}}, {"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}}, }; Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n"); @@ -837,7 +838,7 @@ TEST(PayloadStoreTest, PromoteRehashesBytesTamperedAfterStaging) { const std::filesystem::path prepared = temporary.root() / "prepared"; Write(prepared / "libroblox.so", "native-library"); Write(prepared / "sober_apk/base.apk", "base-apk"); - Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk"); + Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; @@ -853,8 +854,8 @@ TEST(PayloadStoreTest, PromoteRehashesBytesTamperedAfterStaging) { {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")}, - {"x86_64_split_apk", - HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}}, + {std::string(compat::kGuestSplitApkHashKey), + HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}}, {"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}}, }; Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n"); @@ -897,7 +898,7 @@ TEST(PayloadStoreTest, PromoteRejectsALibraryTamperedAfterStaging) { const std::filesystem::path prepared = temporary.root() / "prepared"; Write(prepared / "libroblox.so", "native-library"); Write(prepared / "sober_apk/base.apk", "base-apk"); - Write(prepared / "sober_apk/split_config.x86_64.apk", "split-apk"); + Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; @@ -913,8 +914,8 @@ TEST(PayloadStoreTest, PromoteRejectsALibraryTamperedAfterStaging) { {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")}, - {"x86_64_split_apk", - HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}}, + {std::string(compat::kGuestSplitApkHashKey), + HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}}, {"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}}, }; Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n"); @@ -958,7 +959,7 @@ TEST(PayloadStoreTest, RequiresTwoRuntimeBoundCanariesForLatestCandidate) { const std::filesystem::path prepared = temporary.root() / "prepared"; Write(prepared / "libroblox.so", "new-native-library"); Write(prepared / "sober_apk/base.apk", "new-base-apk"); - Write(prepared / "sober_apk/split_config.x86_64.apk", "new-split-apk"); + Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "new-split-apk"); Write(prepared / "assets/content/fixture", "new-asset"); std::string error; std::size_t asset_count = 0; @@ -972,13 +973,13 @@ TEST(PayloadStoreTest, RequiresTwoRuntimeBoundCanariesForLatestCandidate) { {"package", "com.roblox.client"}, {"version_name", "2.732.1043"}, {"version_code", 2814}, - {"abi", "x86_64"}, + {"abi", std::string(compat::kGuestAbi)}, {"elf_build_id", kBuildId}, {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.apk")}, - {"x86_64_split_apk", - HashRegularFile(prepared / "sober_apk/split_config.x86_64.apk")}}}, + {std::string(compat::kGuestSplitApkHashKey), + HashRegularFile(prepared / "sober_apk" / compat::kGuestSplitApkFile)}}}, {"assets", {{"file_count", asset_count}, {"sha256_tree", asset_hash}}}, }; Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n"); diff --git a/tests/runtime_paths_test.cc b/tests/runtime_paths_test.cc index ca1004f..c769b16 100644 --- a/tests/runtime_paths_test.cc +++ b/tests/runtime_paths_test.cc @@ -1,5 +1,7 @@ #include "runtime/runtime_paths.h" +#include "compat/guest_abi.h" + #include #include @@ -93,8 +95,8 @@ void WriteManagedPayloadFiles(const std::filesystem::path& payload) { ASSERT_TRUE(RuntimePaths::EnsureDirectory(payload / "sober_apk")); std::ofstream(payload / "libroblox.so") << "ELF fixture"; std::ofstream(payload / "sober_apk/base.apk") << "base APK fixture"; - std::ofstream(payload / "sober_apk/split_config.x86_64.apk") - << "x86_64 split APK fixture"; + std::ofstream(payload / "sober_apk" / compat::kGuestSplitApkFile) + << "split APK fixture"; } TEST(RuntimePathsTest, DerivesXdgDefaults) { @@ -174,8 +176,8 @@ TEST(RuntimePathsTest, ResolvesValidatedActivePayload) { EXPECT_EQ(active.root, std::filesystem::canonical(payload)); EXPECT_EQ(active.roblox_library, active.root / "libroblox.so"); EXPECT_EQ(active.base_apk, active.root / "sober_apk/base.apk"); - EXPECT_EQ(active.x86_64_split_apk, - active.root / "sober_apk/split_config.x86_64.apk"); + EXPECT_EQ(active.arch_split_apk, + active.root / "sober_apk" / compat::kGuestSplitApkFile); EXPECT_EQ(active.assets_content, active.root / "assets/content"); EXPECT_EQ(paths.DefaultAssetPath(), active.assets_content); } @@ -189,7 +191,7 @@ TEST(RuntimePathsTest, RejectsIncompleteActiveAndroidPackage) { const std::filesystem::path payload = data / "payloads" / payload_id; WriteManagedPayloadFiles(payload); ASSERT_TRUE(std::filesystem::remove( - payload / "sober_apk/split_config.x86_64.apk")); + payload / "sober_apk" / compat::kGuestSplitApkFile)); std::ofstream(data / "current.json") << "{\"schema_version\":1,\"payload_id\":\"" << payload_id << "\",\"payload_path\":\"payloads/" << payload_id @@ -205,7 +207,8 @@ TEST(RuntimePathsTest, RejectsIncompleteActiveAndroidPackage) { .ResolveActivePayload(); EXPECT_FALSE(active); - EXPECT_NE(active.error.find("split_config.x86_64.apk"), std::string::npos); + EXPECT_NE(active.error.find(std::string(compat::kGuestSplitApkFile)), + std::string::npos); } TEST(RuntimePathsTest, ResolvesCompleteApprovedProfileReferences) { @@ -496,8 +499,8 @@ TEST(RuntimePathsTest, PreparesManagedPayloadRelativeAssetRoot) { active.root = payload; active.roblox_library = payload / "libroblox.so"; active.base_apk = payload / "sober_apk/base.apk"; - active.x86_64_split_apk = - payload / "sober_apk/split_config.x86_64.apk"; + active.arch_split_apk = + payload / "sober_apk" / compat::kGuestSplitApkFile; active.assets_content = payload / "assets/content"; std::string error; diff --git a/tests/vulkan_icd_selection_test.cc b/tests/vulkan_icd_selection_test.cc index c268db1..0927bec 100644 --- a/tests/vulkan_icd_selection_test.cc +++ b/tests/vulkan_icd_selection_test.cc @@ -9,6 +9,8 @@ #include "runtime/graphics_launch_policy.h" +#include "compat/guest_abi.h" + namespace mocktail { namespace runtime { namespace { @@ -40,16 +42,22 @@ void WriteManifest(const std::filesystem::path& path) { TEST(VulkanIcdSelectionTest, PrefersTheNativeArchitectureManifest) { TemporaryDirectory temporary; WriteManifest(temporary.root() / "nvidia_icd.i686.json"); - WriteManifest(temporary.root() / "nvidia_icd.x86_64.json"); + const std::string native_name = + "nvidia_icd." + std::string(compat::kGuestCpuName) + ".json"; + WriteManifest(temporary.root() / native_name); EXPECT_EQ(SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd"), - (temporary.root() / "nvidia_icd.x86_64.json").string()); + (temporary.root() / native_name).string()); } TEST(VulkanIcdSelectionTest, NeverSelectsAForeignArchitectureManifest) { TemporaryDirectory temporary; WriteManifest(temporary.root() / "nvidia_icd.i686.json"); +#if defined(__aarch64__) + WriteManifest(temporary.root() / "radeon_icd.x86_64.json"); +#else WriteManifest(temporary.root() / "radeon_icd.aarch64.json"); +#endif EXPECT_TRUE( SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd").empty()); @@ -70,7 +78,8 @@ TEST(VulkanIcdSelectionTest, KeepsDirectoryPrecedenceOverName) { const std::filesystem::path first = temporary.root() / "first"; const std::filesystem::path second = temporary.root() / "second"; WriteManifest(first / "nvidia_icd.json"); - WriteManifest(second / "nvidia_icd.x86_64.json"); + WriteManifest(second / ("nvidia_icd." + + std::string(compat::kGuestCpuName) + ".json")); EXPECT_EQ(SelectVulkanIcdManifest({first, second}, "nvidia_icd"), (first / "nvidia_icd.json").string()); @@ -78,13 +87,15 @@ TEST(VulkanIcdSelectionTest, KeepsDirectoryPrecedenceOverName) { TEST(VulkanIcdSelectionTest, IsStableWhateverTheDirectoryOrderIs) { TemporaryDirectory temporary; - WriteManifest(temporary.root() / "nvidia_icd.x86_64.json"); + const std::string native_name = + "nvidia_icd." + std::string(compat::kGuestCpuName) + ".json"; + WriteManifest(temporary.root() / native_name); WriteManifest(temporary.root() / "nvidia_icd.json"); WriteManifest(temporary.root() / "nvidia_icd.i686.json"); const std::string selected = SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd"); - EXPECT_EQ(selected, (temporary.root() / "nvidia_icd.x86_64.json").string()); + EXPECT_EQ(selected, (temporary.root() / native_name).string()); EXPECT_EQ(selected, SelectVulkanIcdManifest({temporary.root()}, "nvidia_icd")); }