diff --git a/config/roblox_compatibility.json b/config/roblox_compatibility.json index b39e69c..67ff302 100644 --- a/config/roblox_compatibility.json +++ b/config/roblox_compatibility.json @@ -62,6 +62,26 @@ "select_method_rva": "0x32734a6" }, "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": "ade08266c67aee88ec9c1d00902150e1684dad3a", + "status": "supported", + "default_allowed": true, + "allow_legacy_binary_patches": false, + "allow_host_abi_bridges": true, + "allow_host_constructor_replay": true, + "user_game_settings_fullscreen_setter_rva": "0x45ad8aa", + "fmod_output_device_bridge": { + "vtable_rva": "0x6c58040", + "string_constructor_rva": "0x1d32df8", + "count_method_rva": "0x32d0ee0", + "info_method_rva": "0x32d0f80", + "current_method_rva": "0x32d0f30", + "select_method_rva": "0x32d0cb4" + }, + "reason": "Verified 2026-09-06: exact Build-ID profile passed two isolated no-recovery Tier C Vulkan canaries and real game sessions, including fullscreen state synchronization and host FMOD output-device interposition; legacy executable patches and signal recovery remain disabled." } ] } diff --git a/config/roblox_host_abi_reference.json b/config/roblox_host_abi_reference.json index 0c94e4f..6755f88 100644 --- a/config/roblox_host_abi_reference.json +++ b/config/roblox_host_abi_reference.json @@ -1,64 +1,64 @@ { "schema_version": 1, - "elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326", - "payload_sha256": "72c3b1a5c151b5ac0bfc5394ce3a91775884fbd777589ccab8ac1c4f9885f739", - "payload_id": "2908-63c5109637b7d7b2bdb8ed8f858023ff5ef49326", - "payload_path": "payloads/2908-63c5109637b7d7b2bdb8ed8f858023ff5ef49326", + "elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a", + "payload_sha256": "4343a6a900d1fca27ff8e10b9d9c86bee40a5d0ba547cb29903b7769f4f42a9d", + "payload_id": "2998-ade08266c67aee88ec9c1d00902150e1684dad3a", + "payload_path": "payloads/2998-ade08266c67aee88ec9c1d00902150e1684dad3a", "reference": { - "elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13", - "payload_sha256": "3e9c26c81186f93458ff65d8a8bc240c220974db02b8388b91340b77b336997d" + "elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326", + "payload_sha256": "72c3b1a5c151b5ac0bfc5394ce3a91775884fbd777589ccab8ac1c4f9885f739" }, "profile": { - "elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326", + "elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a", "bridge_entries": [ { - "rva": "0x1cf69e2", + "rva": "0x1d2c1e2", "kind": "allocate", "label": "small-allocate" }, { - "rva": "0x1cf9c3a", + "rva": "0x1d2f43a", "kind": "usable_size", "label": "usable-size" }, { - "rva": "0x21af33f", + "rva": "0x21e68bf", "kind": "reallocate", "label": "reallocate" }, { - "rva": "0x1cf677e", + "rva": "0x1d2bf82", "kind": "allocate", "label": "allocate" }, { - "rva": "0x1d12197", + "rva": "0x1d47a63", "kind": "aligned_allocate", "label": "aligned-allocate-direct" }, { - "rva": "0x1cfa1d9", + "rva": "0x1d2f9d9", "kind": "free", "label": "free" } ], "data_seeds": { - "allocator_object_slot": "0x7571570", - "empty_string_slot": "0x767e428", - "jni_singleton_slot": "0x7ad47e8", + "allocator_object_slot": "0x7652e70", + "empty_string_slot": "0x775fd28", + "jni_singleton_slot": "0x7bb4340", "jni_singleton_bytes": 1024, - "arena_initializer": "0x1cf6df8", - "allocator_thread_initializer": "0x1d82be7", - "arena_guard_slot": "0x7a10730", - "arena_table_slot": "0x7a107c0", + "arena_initializer": "0x1d2c5f8", + "allocator_thread_initializer": "0x1db7b57", + "arena_guard_slot": "0x7af0430", + "arena_table_slot": "0x7af04c0", "arena_table_slot_count": 4194304 }, "native_allocator": { - "allocate": "0x1cf677e", - "deallocate": "0x1cfa1d9" + "allocate": "0x1d2bf82", + "deallocate": "0x1d2f9d9" }, - "init_array_offset": "0x6f78a18", - "init_array_count": 3556, + "init_array_offset": "0x7057818", + "init_array_count": 3570, "constructor_run_ranges": [ { "begin": 2, @@ -66,32 +66,32 @@ }, { "begin": 5, - "end_exclusive": 3556 + "end_exclusive": 3570 } ], "native_mimalloc_constructor_run_ranges": [ { "begin": 2, - "end_exclusive": 3556 + "end_exclusive": 3570 } ], "native_mimalloc_thread_initializer_after_constructor": 2, "native_pre_jni_bootstrap": { - "registry_initializer": "0x21b1889", - "registry_slot": "0x7a14898" + "registry_initializer": "0x21e8e09", + "registry_slot": "0x7af4598" }, "default_allocator_strategy": "native_mimalloc" }, "derivation_anchors": { "signature_version": 1, - "allocator_object_initializer_rva": "0x2186a91", - "empty_string_initializer_rva": "0x2192669", - "jni_singleton_initializer_rva": "0x1cf6309", + "allocator_object_initializer_rva": "0x21bde7d", + "empty_string_initializer_rva": "0x21c9a86", + "jni_singleton_initializer_rva": "0x1d2aec0", "constructor_rvas": { - "2": "0x1cf9f31", - "3": "0x1cf5680", - "4": "0x1cf63c8", - "5": "0x1cf6309" + "2": "0x1d2f731", + "3": "0x1d2af84", + "4": "0x1d2bc08", + "5": "0x1d2aec0" } } } diff --git a/config/roblox_payload.json b/config/roblox_payload.json index 60605bc..f969c20 100644 --- a/config/roblox_payload.json +++ b/config/roblox_payload.json @@ -1,23 +1,24 @@ { "schema_version": 1, "package": "com.roblox.client", - "version_name": "2.725.1142", - "version_code": 2546, + "version_name": "2.736.1408", + "version_code": 2998, "abi": "x86_64", - "elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21", + "elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a", "sha256": { - "libroblox": "92ca0e4d8bc4e1c7a37fc2b52544869299ec8b9a40ea8da96ebac66660e0b1af", - "base_apk": "eba476fec3020eaceb28436c1d160eb35d6689d8ffb060e09678535584babc7a", - "x86_64_split_apk": "5c4193c9700481079cee958b38ba9cbb97768b0a130b7f704ed084753f01ce2b" + "libroblox": "4343a6a900d1fca27ff8e10b9d9c86bee40a5d0ba547cb29903b7769f4f42a9d", + "base_apk": "a4a71ec39df6619a56ab7adce0959d1d60354835485b53806f4026914287ea86", + "x86_64_split_apk": "a4a71ec39df6619a56ab7adce0959d1d60354835485b53806f4026914287ea86" }, "signing_certificates_sha256": [ - "2bebd189e8d3106401347056c93d045b61e20e22d0c3cbed85474aeb00a3d12a", "44932ea35a17a267372d71b54d1a0cb3da0dca5113e94406ae2fe18090ba1477" ], "assets": { - "file_count": 1837 + "file_count": 1836, + "sha256_tree": "caee48e11c194bc11852493f53ff67d5e2bb233172f12b87ea5fca97c0c10840" }, - "source": "sober-update-service", - "imported_at": "2026-07-09T19:12:54Z", - "compatibility_status": "supported" + "source": "apk-pure-native", + "imported_at": "2026-08-31T16:30:19Z", + "compatibility_status": "supported", + "apk_layout": "monolithic" } diff --git a/src/compat/host_abi_profile.cc b/src/compat/host_abi_profile.cc index 0b3ae91..a769381 100644 --- a/src/compat/host_abi_profile.cc +++ b/src/compat/host_abi_profile.cc @@ -6,6 +6,29 @@ namespace mocktail::compat { namespace { constexpr HostAbiProfile kProfiles[] = { + // Roblox 2.736.1408. Derived from the verified 2908 profile and validated + // by two isolated no-recovery Vulkan canaries and real game sessions. + {"ade08266c67aee88ec9c1d00902150e1684dad3a", + {{{0x1d2c1e2, HostBridgeKind::kAllocate, "small-allocate"}, + {0x1d2f43a, HostBridgeKind::kUsableSize, "usable-size"}, + {0x21e68bf, HostBridgeKind::kReallocate, "reallocate"}, + {0x1d2bf82, HostBridgeKind::kAllocate, "allocate"}, + {0x1d47a63, HostBridgeKind::kAlignedAllocate, + "aligned-allocate-direct"}, + {0x1d2f9d9, HostBridgeKind::kFree, "free"}}}, + 6, + {0x7652e70, 0x775fd28, 0x7bb4340, 0x400, 0x1d2c5f8, 0x1db7b57, + 0x7af0430, 0x7af04c0, 0x400000}, + {0x1d2bf82, 0x1d2f9d9}, + 0x7057818, + 3570, + {{{2, 3}, {5, 3570}}}, + 2, + {{{2, 3570}}}, + 1, + 2, + {0x21e8e09, 0x7af4598}, + HostAllocatorStrategy::kNativeMimalloc}, // Roblox 2.734.917. This exact Build-ID profile was machine-derived from // the verified reference and passed two isolated no-recovery Tier C Vulkan // canaries with native constructors, JNI startup, presentation, and diff --git a/tests/build_profile_test.cc b/tests/build_profile_test.cc index 46c5616..e081eeb 100644 --- a/tests/build_profile_test.cc +++ b/tests/build_profile_test.cc @@ -102,6 +102,38 @@ TEST(BuildProfileTest, ReportsUnknownBuildWithoutInventingProfile) { EXPECT_FALSE(result.profile.has_value()); } +TEST(BuildProfileTest, Payload2998SupportsFullscreenAndHostAudioByDefault) { + const ProfileLookupResult result = FindBuildProfile( + kManifestPath, "ade08266c67aee88ec9c1d00902150e1684dad3a"); + + ASSERT_TRUE(result) << result.error; + ASSERT_TRUE(result.profile.has_value()); + EXPECT_EQ(result.profile->version_name, "2.736.1408"); + EXPECT_EQ(result.profile->version_code, 2998); + EXPECT_EQ(result.profile->status, BuildStatus::kSupported); + EXPECT_TRUE(result.profile->default_allowed); + EXPECT_FALSE(result.profile->allow_legacy_binary_patches); + EXPECT_TRUE(result.profile->allow_host_abi_bridges); + EXPECT_TRUE(result.profile->allow_host_constructor_replay); + ASSERT_TRUE( + result.profile->user_game_settings_fullscreen_setter_rva.has_value()); + EXPECT_EQ(*result.profile->user_game_settings_fullscreen_setter_rva, + 0x45ad8aaU); + ASSERT_TRUE(result.profile->fmod_output_device_bridge.has_value()); + EXPECT_EQ(result.profile->fmod_output_device_bridge->vtable_rva, + 0x6c58040U); + EXPECT_EQ(result.profile->fmod_output_device_bridge->string_constructor_rva, + 0x1d32df8U); + EXPECT_EQ(result.profile->fmod_output_device_bridge->count_method_rva, + 0x32d0ee0U); + EXPECT_EQ(result.profile->fmod_output_device_bridge->info_method_rva, + 0x32d0f80U); + EXPECT_EQ(result.profile->fmod_output_device_bridge->current_method_rva, + 0x32d0f30U); + EXPECT_EQ(result.profile->fmod_output_device_bridge->select_method_rva, + 0x32d0cb4U); +} + TEST(BuildProfileTest, RejectsInvalidBuildId) { const ProfileLookupResult result = FindBuildProfile(kManifestPath, "oops"); diff --git a/tests/derive_roblox_host_abi_profile_test.py b/tests/derive_roblox_host_abi_profile_test.py index 90c63be..5710235 100644 --- a/tests/derive_roblox_host_abi_profile_test.py +++ b/tests/derive_roblox_host_abi_profile_test.py @@ -31,7 +31,8 @@ import unittest PROJECT_ROOT = Path(__file__).resolve().parents[1] ANALYZER_PATH = PROJECT_ROOT / "scripts" / "derive_roblox_host_abi_profile.py" -REFERENCE_PROFILE_PATH = PROJECT_ROOT / "config" / "roblox_host_abi_reference.json" +# Keep the upgrade fixture independent of the project's current reference. +REFERENCE_PROFILE_PATH = PROJECT_ROOT / "tests" / "fixtures" / "roblox_host_abi_2908.json" REFERENCE_PAYLOAD_ID = "2908-63c5109637b7d7b2bdb8ed8f858023ff5ef49326" CANDIDATE_PAYLOAD_ID = "2998-ade08266c67aee88ec9c1d00902150e1684dad3a" PAYLOAD_ROOT = Path.home() / ".local" / "share" / "mocktail" / "payloads" diff --git a/tests/fixtures/roblox_host_abi_2908.json b/tests/fixtures/roblox_host_abi_2908.json new file mode 100644 index 0000000..0c94e4f --- /dev/null +++ b/tests/fixtures/roblox_host_abi_2908.json @@ -0,0 +1,97 @@ +{ + "schema_version": 1, + "elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326", + "payload_sha256": "72c3b1a5c151b5ac0bfc5394ce3a91775884fbd777589ccab8ac1c4f9885f739", + "payload_id": "2908-63c5109637b7d7b2bdb8ed8f858023ff5ef49326", + "payload_path": "payloads/2908-63c5109637b7d7b2bdb8ed8f858023ff5ef49326", + "reference": { + "elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13", + "payload_sha256": "3e9c26c81186f93458ff65d8a8bc240c220974db02b8388b91340b77b336997d" + }, + "profile": { + "elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326", + "bridge_entries": [ + { + "rva": "0x1cf69e2", + "kind": "allocate", + "label": "small-allocate" + }, + { + "rva": "0x1cf9c3a", + "kind": "usable_size", + "label": "usable-size" + }, + { + "rva": "0x21af33f", + "kind": "reallocate", + "label": "reallocate" + }, + { + "rva": "0x1cf677e", + "kind": "allocate", + "label": "allocate" + }, + { + "rva": "0x1d12197", + "kind": "aligned_allocate", + "label": "aligned-allocate-direct" + }, + { + "rva": "0x1cfa1d9", + "kind": "free", + "label": "free" + } + ], + "data_seeds": { + "allocator_object_slot": "0x7571570", + "empty_string_slot": "0x767e428", + "jni_singleton_slot": "0x7ad47e8", + "jni_singleton_bytes": 1024, + "arena_initializer": "0x1cf6df8", + "allocator_thread_initializer": "0x1d82be7", + "arena_guard_slot": "0x7a10730", + "arena_table_slot": "0x7a107c0", + "arena_table_slot_count": 4194304 + }, + "native_allocator": { + "allocate": "0x1cf677e", + "deallocate": "0x1cfa1d9" + }, + "init_array_offset": "0x6f78a18", + "init_array_count": 3556, + "constructor_run_ranges": [ + { + "begin": 2, + "end_exclusive": 3 + }, + { + "begin": 5, + "end_exclusive": 3556 + } + ], + "native_mimalloc_constructor_run_ranges": [ + { + "begin": 2, + "end_exclusive": 3556 + } + ], + "native_mimalloc_thread_initializer_after_constructor": 2, + "native_pre_jni_bootstrap": { + "registry_initializer": "0x21b1889", + "registry_slot": "0x7a14898" + }, + "default_allocator_strategy": "native_mimalloc" + }, + "derivation_anchors": { + "signature_version": 1, + "allocator_object_initializer_rva": "0x2186a91", + "empty_string_initializer_rva": "0x2192669", + "jni_singleton_initializer_rva": "0x1cf6309", + "constructor_rvas": { + "2": "0x1cf9f31", + "3": "0x1cf5680", + "4": "0x1cf63c8", + "5": "0x1cf6309" + } + } +} diff --git a/tests/native_update_test.cc b/tests/native_update_test.cc index 0bbf280..bf6a4a4 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/host_abi_profile.h" #include "update/apkpure_provider.h" #include "update/compatibility_catalog.h" #include "update/host_abi_deriver.h" @@ -545,6 +546,57 @@ TEST(ShippedMetadataTest, ReferenceSidecarDescribesASupportedProfile) { EXPECT_TRUE(runtime.profile->fmod_output_device_bridge.has_value()); } +TEST(ShippedMetadataTest, DefaultPayloadHasABuiltinProfileAndMatchingReference) { + const std::filesystem::path root = MOCKTAIL_TEST_SOURCE_DIR; + const auto catalog = + LoadCompatibilityCatalog(root / "config/roblox_compatibility.json"); + ASSERT_TRUE(catalog) << catalog.error; + const auto preferred = PreferredSupportedProfile(catalog.profiles); + ASSERT_TRUE(preferred.has_value()); + EXPECT_EQ(preferred->version_name, "2.736.1408"); + EXPECT_EQ(preferred->version_code, 2998U); + + const auto payload = nlohmann::json::parse( + ReadFile(root / "config/roblox_payload.json")); + EXPECT_EQ(payload.at("version_name"), preferred->version_name); + EXPECT_EQ(payload.at("version_code"), preferred->version_code); + EXPECT_EQ(payload.at("elf_build_id"), preferred->elf_build_id); + EXPECT_EQ(payload.at("compatibility_status"), "supported"); + + const auto reference = nlohmann::json::parse( + ReadFile(root / "config/roblox_host_abi_reference.json")); + EXPECT_EQ(reference.at("payload_sha256"), + payload.at("sha256").at("libroblox")); + const auto* builtin = compat::FindHostAbiProfile(preferred->elf_build_id); + ASSERT_NE(builtin, nullptr); + const auto& profile = reference.at("profile"); + const auto rva = [](const nlohmann::json& value) { + return std::stoull(value.get(), nullptr, 16); + }; + EXPECT_EQ(builtin->init_array_offset, rva(profile.at("init_array_offset"))); + EXPECT_EQ(builtin->init_array_count, profile.at("init_array_count")); + ASSERT_EQ(builtin->bridge_entry_count, profile.at("bridge_entries").size()); + for (std::size_t i = 0; i < builtin->bridge_entry_count; ++i) { + EXPECT_EQ(builtin->bridge_entries[i].rva, + rva(profile.at("bridge_entries").at(i).at("rva"))); + } + EXPECT_EQ(builtin->native_allocator.allocate, + rva(profile.at("native_allocator").at("allocate"))); + EXPECT_EQ(builtin->native_allocator.deallocate, + rva(profile.at("native_allocator").at("deallocate"))); + EXPECT_EQ(builtin->native_pre_jni_bootstrap.registry_initializer, + rva(profile.at("native_pre_jni_bootstrap") + .at("registry_initializer"))); + EXPECT_EQ(builtin->native_pre_jni_bootstrap.registry_slot, + rva(profile.at("native_pre_jni_bootstrap").at("registry_slot"))); + EXPECT_TRUE(builtin->HasValidConstructorRanges()); + EXPECT_TRUE(builtin->HasValidNativeMimallocConstructorRanges()); + EXPECT_EQ(builtin->NativeMimallocConstructorRangeEndExclusive(), + builtin->init_array_count); + EXPECT_EQ(builtin->default_allocator_strategy, + compat::HostAllocatorStrategy::kNativeMimalloc); +} + std::string ActivationManifestFixture(const std::string& payload_id) { return nlohmann::json({{"schema_version", 1}, {"payload_id", payload_id},