Co-authored-by: HttpAnimations <shawndouglasyt@gmail.com>
This commit is contained in:
珍惜andHttpAnimations authored and GitHub committed 2026-09-21 20:09:37 +03:00
1 parent 854969457f
commit 40237fae72
29 files changed
+562 -179

No files matched your search

+5
View File
@@ -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)
+11
View File
@@ -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,
+29
View File
@@ -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;
@@ -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(
+38
View File
@@ -0,0 +1,38 @@
#ifndef MOCKTAIL_COMPAT_GUEST_ABI_H_
#define MOCKTAIL_COMPAT_GUEST_ABI_H_
#include <elf.h>
#include <string_view>
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_
+6
View File
@@ -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 {
@@ -6,6 +6,9 @@
#if defined(__x86_64__)
#include <xmmintrin.h>
#endif
#if defined(__aarch64__)
#include <cstdint>
#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
};
+1 -1
View File
@@ -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;
+8 -2
View File
@@ -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
+16 -2
View File
@@ -16,6 +16,8 @@
#include <cstdlib>
#include <cstring>
#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) {
+166 -71
View File
@@ -1,10 +1,14 @@
#include "compat/bionic_pthread_create_runtime.h"
#include "compat/bionic_abi_exports.h"
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <algorithm>
#include <atomic>
#include <cstring>
#include <limits>
#include <new>
@@ -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<size_t>(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, &parameters);
}
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);
}
+11
View File
@@ -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<uintptr_t>(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<uintptr_t>(target);
std::memcpy(patch + 2, &target_address, sizeof(target_address));
#endif
const uintptr_t page_mask = static_cast<uintptr_t>(page_size) - 1;
const uintptr_t page = address & ~page_mask;
+28
View File
@@ -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",
+19 -10
View File
@@ -32,6 +32,7 @@
#include <openssl/evp.h>
#include "compat/elf_build_id.h"
#include "compat/guest_abi.h"
namespace mocktail::compat {
namespace {
@@ -611,19 +612,26 @@ std::unique_ptr<OwnedHostAbiProfile> 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;
}
+53 -8
View File
@@ -36,7 +36,9 @@
#include <fcntl.h>
#include <unistd.h>
#include <sys/prctl.h>
#if defined(__x86_64__)
#include <asm/prctl.h>
#endif
#include <sys/syscall.h>
#include <vector>
@@ -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<void>(info);
auto* uc = static_cast<ucontext_t*>(context);
#if defined(__x86_64__)
auto rip = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RIP]) : 0;
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RSP]) : 0;
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RBP]) : 0;
auto rax = uc ? static_cast<uintptr_t>(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<uintptr_t>(uc->uc_mcontext.pc) : 0;
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.sp) : 0;
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.regs[29]) : 0;
auto rax = uc ? static_cast<uintptr_t>(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<unsigned long long>(rip), static_cast<unsigned long long>(rsp),
static_cast<unsigned long long>(rbp), static_cast<unsigned long long>(rax));
write(2, regs_msg, static_cast<size_t>(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<void*>(mocktail_pthread_spin_trylock));
linker::RegisterSymbol("pthread_spin_unlock",
reinterpret_cast<void*>(mocktail_pthread_spin_unlock));
linker::RegisterSymbol(
"pthread_attr_init",
reinterpret_cast<void*>(mocktail_pthread_attr_init));
linker::RegisterSymbol(
"pthread_attr_destroy",
reinterpret_cast<void*>(mocktail_pthread_attr_destroy));
linker::RegisterSymbol(
"pthread_attr_setstacksize",
reinterpret_cast<void*>(mocktail_pthread_attr_setstacksize));
linker::RegisterSymbol(
"pthread_attr_setdetachstate",
reinterpret_cast<void*>(mocktail_pthread_attr_setdetachstate));
linker::RegisterSymbol(
"pthread_attr_setschedparam",
reinterpret_cast<void*>(mocktail_pthread_attr_setschedparam));
linker::RegisterSymbol(
"pthread_getattr_np",
reinterpret_cast<void*>(mocktail_pthread_getattr_np));
linker::RegisterSymbol(
"pthread_attr_getstack",
reinterpret_cast<void*>(mocktail_pthread_attr_getstack));
linker::RegisterSymbol("pthread_barrier_init",
reinterpret_cast<void*>(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;
}
+5 -3
View File
@@ -5,6 +5,7 @@
#include <string>
#include <string_view>
#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 <path> 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 <backend> direct-vulkan | opengl | system | "
+9
View File
@@ -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;
+7 -4
View File
@@ -14,6 +14,8 @@
#include <unistd.h>
#include <utility>
#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<std::pair<std::filesystem::path, const char*>, 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 =
+25 -13
View File
@@ -28,6 +28,7 @@
#include <vector>
#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<std::filesystem::path>& archives,
const std::filesystem::path& directory,
std::vector<std::filesystem::path>* candidates,
@@ -250,7 +256,7 @@ bool InspectCandidates(const std::vector<std::filesystem::path>& 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<std::string> 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<std::string> 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<std::string> 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},
+4 -2
View File
@@ -9,6 +9,7 @@
#include <string>
#include <utility>
#include "compat/guest_abi.h"
#include "update/http_download.h"
namespace mocktail::update {
@@ -199,7 +200,8 @@ std::vector<std::string> 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;
+2 -1
View File
@@ -19,6 +19,7 @@
#include <string_view>
#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=")
+5 -3
View File
@@ -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<Json> 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() ||
+6 -3
View File
@@ -18,6 +18,8 @@
#include <thread>
#include <vector>
#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<std::pair<std::filesystem::path, std::string>, 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 =
+6 -2
View File
@@ -1,5 +1,7 @@
#include "compat/bionic_abi_exports.h"
#include "compat/guest_abi.h"
#include <gtest/gtest.h>
#include <fcntl.h>
@@ -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<int>(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()));
+18 -14
View File
@@ -5,6 +5,8 @@
#include <sched.h>
#include <unistd.h>
#include "compat/bionic_abi_exports.h"
#include <algorithm>
#include <atomic>
@@ -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, &parameters), 0);
ASSERT_EQ(
mocktail_pthread_attr_setschedparam(&attributes, &parameters), 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);
}
}
+6 -2
View File
@@ -2,6 +2,8 @@
#include <gtest/gtest.h>
#include "compat/guest_abi.h"
#include <algorithm>
#include <array>
#include <string>
@@ -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);
+20 -19
View File
@@ -13,6 +13,7 @@
#include <vector>
#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");
+11 -8
View File
@@ -1,5 +1,7 @@
#include "runtime/runtime_paths.h"
#include "compat/guest_abi.h"
#include <gtest/gtest.h>
#include <unistd.h>
@@ -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;
+16 -5
View File
@@ -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"));
}