#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "compat/build_profile.h" #include "compat/elf_build_id.h" #include "compat/guest_abi.h" #include "compat/host_abi_profile.h" #include "update/apkpure_provider.h" #include "update/compatibility_catalog.h" #include "update/host_abi_deriver.h" #include "update/http_download.h" #include "update/payload_integrity.h" #include "update/payload_store.h" #include "update/readiness_canary.h" #include "update/unsafe_latest_runner.h" #include "update/update_config.h" #include "update/update_coordinator.h" #include "update/zip_archive.h" namespace mocktail::update { namespace { class TemporaryDirectory final { public: TemporaryDirectory() { char pattern[] = "/tmp/mocktail_native_update_XXXXXX"; const char* created = mkdtemp(pattern); if (created != nullptr) root_ = created; } ~TemporaryDirectory() { std::error_code error; std::filesystem::remove_all(root_, error); } const std::filesystem::path& root() const { return root_; } private: std::filesystem::path root_; }; void Write(const std::filesystem::path& path, std::string_view contents) { std::filesystem::create_directories(path.parent_path()); std::ofstream(path, std::ios::binary) << contents; // Approval artifacts are rejected when group or world writable, which a // permissive umask would otherwise leave them. chmod(path.c_str(), 0600); } std::string ReadFile(const std::filesystem::path& path) { std::ifstream input(path, std::ios::binary); return std::string(std::istreambuf_iterator(input), std::istreambuf_iterator()); } std::string MetadataRecord(std::string_view version_name, std::uint64_t version_code, std::string_view artifact, std::string_view url) { const std::string code = std::to_string(version_code); std::string record("protobuf-prefix\0", 16); record.push_back('\x2a'); record.push_back(static_cast(code.size())); record += code; record.push_back('\x32'); record.push_back(static_cast(version_name.size())); record += version_name; record += ":metadata"; record.push_back('\0'); record += artifact; record.append("\0\0", 2); record += url; record.push_back('\0'); return record; } bool CreateZip(const std::filesystem::path& path) { zipFile archive = zipOpen64(path.c_str(), APPEND_STATUS_CREATE); if (archive == nullptr) return false; const std::vector> entries = { {"ignored.txt", "ignored"}, {"assets/content/first.txt", "first"}, {"assets/nested/second.txt", "second"}, }; for (const auto& [name, contents] : entries) { if (zipOpenNewFileInZip64(archive, name.c_str(), nullptr, nullptr, 0, nullptr, 0, nullptr, Z_DEFLATED, Z_DEFAULT_COMPRESSION, 1) != ZIP_OK || zipWriteInFileInZip(archive, contents.data(), contents.size()) != ZIP_OK || zipCloseFileInZip(archive) != ZIP_OK) { zipClose(archive, nullptr); return false; } } return zipClose(archive, nullptr) == ZIP_OK; } TEST(NativeUpdateConfigTest, ReadsOnlyTypedUpdaterSection) { TemporaryDirectory temporary; const auto config = temporary.root() / "config.yaml"; Write(config, "version: 1\n" "device: pc-windows-11\n" "updates:\n" " automatic: false\n" " source: apk-pure\n" " launch_after_update: true\n"); const UpdateConfigResult result = LoadUpdateConfig(config); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.file_loaded); EXPECT_FALSE(result.config.automatic); EXPECT_TRUE(result.config.launch_after_update); EXPECT_EQ(result.config.source, "apk-pure"); EXPECT_TRUE(result.warnings.empty()); } TEST(NativeUpdateConfigTest, IgnoresDeprecatedTestingLatestOnlyWithWarning) { TemporaryDirectory temporary; const auto config = temporary.root() / "config.yaml"; Write(config, "version: 1\nupdates:\n testing_latest_only: true\n"); const UpdateConfigResult result = LoadUpdateConfig(config); ASSERT_TRUE(result) << result.error; ASSERT_EQ(result.warnings.size(), 1U); EXPECT_NE(result.warnings.front().find("no longer supported"), std::string::npos); } TEST(NativeUpdateConfigTest, MissingFileUsesSafeDefaults) { TemporaryDirectory temporary; const UpdateConfigResult result = LoadUpdateConfig(temporary.root() / "missing.yaml"); ASSERT_TRUE(result) << result.error; EXPECT_FALSE(result.file_loaded); EXPECT_TRUE(result.config.automatic); EXPECT_EQ(result.config.source, "apk-pure"); } TEST(NativeUpdateConfigTest, RejectsUnsupportedSource) { TemporaryDirectory temporary; const auto config = temporary.root() / "config.yaml"; Write(config, "version: 1\nupdates:\n source: google-play\n"); const UpdateConfigResult result = LoadUpdateConfig(config); EXPECT_FALSE(result); EXPECT_NE(result.error.find("apk-pure"), std::string::npos); } TEST(ApkPureProviderTest, ParsesExactVersionIdentityAndTrustedUrl) { const std::string metadata = MetadataRecord("2.727.1199", 2628, "XAPKJ", "https://download.pureapk.com/b/XAPK/roblox"); const ProviderVersion latest = ParseApkPureLatestMetadata(metadata); ASSERT_TRUE(latest) << latest.error; EXPECT_EQ(latest.version_name, "2.727.1199"); EXPECT_EQ(latest.version_code, 2628); std::string error; const auto urls = ParseApkPureExactDownloadUrls(metadata, "2.727.1199", &error); ASSERT_TRUE(error.empty()) << error; ASSERT_EQ(urls.size(), 1); EXPECT_EQ(urls.front(), "https://download.pureapk.com/b/XAPK/roblox"); } TEST(UnsafeLatestRunnerTest, LaunchesCandidateWithUnapprovedEnvironment) { TemporaryDirectory temporary; const std::filesystem::path output = temporary.root() / "environment.txt"; const std::filesystem::path arguments = temporary.root() / "arguments.txt"; const std::filesystem::path runtime = temporary.root() / "runtime.sh"; Write(runtime, "#!/bin/sh\n" "env | sort > \"$ENV_OUTPUT\"\n" "printf '%s\\n' \"$@\" > \"$ARGUMENT_OUTPUT\"\n"); ASSERT_EQ(chmod(runtime.c_str(), 0700), 0); const std::filesystem::path payload = temporary.root() / "payload"; Write(payload / "libroblox.so", "candidate"); std::filesystem::create_directories(payload / "assets/content"); const std::filesystem::path compatibility = temporary.root() / "compatibility.json"; const std::filesystem::path profile = temporary.root() / "profile.json"; Write(compatibility, "{}\n"); Write(profile, "{}\n"); UnsafeLatestRunOptions options; options.runtime_binary = runtime; options.payload_directory = payload; options.compatibility_manifest = compatibility; options.host_abi_profile = profile; options.inherited_environment = { "PATH=/usr/bin:/bin", "ENV_OUTPUT=" + output.string(), "ARGUMENT_OUTPUT=" + arguments.string(), "UNCHANGED=value", "ROBLOX_LIB_PATH=/stale/libroblox.so", "MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT=/stale/approval.json", "MOCKTAIL_SKIP_UPDATE_CHECK=0", }; const UnsafeLatestRunResult result = RunUnsafeLatestCandidate(options); ASSERT_TRUE(result) << result.error; const std::string environment = ReadFile(output); EXPECT_NE(environment.find("UNCHANGED=value\n"), std::string::npos); EXPECT_NE(environment.find("ROBLOX_LIB_PATH=" + (payload / "libroblox.so").string() + "\n"), std::string::npos); EXPECT_NE(environment.find("MOCKTAIL_COMPATIBILITY_MANIFEST=" + compatibility.string() + "\n"), std::string::npos); EXPECT_NE(environment.find("MOCKTAIL_HOST_ABI_PROFILE_FILE=" + profile.string() + "\n"), std::string::npos); EXPECT_NE(environment.find("MOCKTAIL_HOST_ABI_CANARY=1\n"), std::string::npos); EXPECT_NE(environment.find("MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI=1\n"), std::string::npos); EXPECT_NE(environment.find("MOCKTAIL_SKIP_UPDATE_CHECK=1\n"), std::string::npos); EXPECT_NE(environment.find("MOCKTAIL_UNSAFE_LATEST=1\n"), std::string::npos); EXPECT_EQ(environment.find("/stale/libroblox.so"), std::string::npos); EXPECT_EQ(environment.find("/stale/approval.json"), std::string::npos); EXPECT_EQ(ReadFile(arguments), "--windowed\n--allow-unverified-build\n"); } TEST(HttpDownloadPolicyTest, RejectsCredentialsAndHostSuffixTricks) { const std::vector hosts = {"pureapk.com"}; EXPECT_TRUE(IsTrustedHttpsUrl("https://download.pureapk.com/a", hosts)); EXPECT_FALSE(IsTrustedHttpsUrl("http://download.pureapk.com/a", hosts)); EXPECT_FALSE(IsTrustedHttpsUrl("https://pureapk.com.attacker.test/a", hosts)); EXPECT_FALSE(IsTrustedHttpsUrl("https://user@pureapk.com/a", hosts)); } TEST(ReadinessCanaryTest, PreservesEtc2DriverOverride) { TemporaryDirectory temporary; CanaryOptions options; options.runtime_binary = temporary.root() / "runtime"; options.cache_root = temporary.root() / "cache"; options.state_root = temporary.root() / "state"; options.payload_directory = temporary.root() / "payload"; options.timeout_seconds = 5; Write(options.runtime_binary, "#!/bin/sh\nprintf 'etc2=%s\\n' \"$vk_require_etc2\"\n"); ASSERT_EQ(chmod(options.runtime_binary.c_str(), 0700), 0); for (const char* value : {"true", "false"}) { const pid_t child = fork(); ASSERT_GE(child, 0); if (child == 0) { if (setenv("vk_require_etc2", value, 1) != 0) std::_Exit(1); const auto result = RunReadinessCanary(options); std::_Exit(result.exit_code == 0 && ReadFile(result.log_path) == std::string("etc2=") + value + "\n" ? 0 : 2); } int status = 0; ASSERT_EQ(waitpid(child, &status, 0), child); ASSERT_TRUE(WIFEXITED(status)); EXPECT_EQ(WEXITSTATUS(status), 0); } } TEST(ReadinessCanaryTest, ReportsTerminatingSignal) { TemporaryDirectory temporary; CanaryOptions options; options.runtime_binary = temporary.root() / "runtime"; options.cache_root = temporary.root() / "cache"; options.state_root = temporary.root() / "state"; options.payload_directory = temporary.root() / "payload"; options.timeout_seconds = 5; Write(options.runtime_binary, "#!/bin/sh\nkill -TERM $$\n"); ASSERT_EQ(chmod(options.runtime_binary.c_str(), 0700), 0); const auto result = RunReadinessCanary(options); EXPECT_FALSE(result); EXPECT_EQ(result.exit_code, 128 + SIGTERM); EXPECT_EQ(result.error, "graphics canary exited with status " + std::to_string(128 + SIGTERM)); } TEST(ReadinessCanaryTest, AcceptsRealPresentWithoutShaderPackSummary) { const std::string valid = "[compat] legacy binary patches: disabled\n" "[compat] signal-recovery handler disabled\n" "[compat] native allocator retained; host allocator bridges disabled\n" "[window] vkQueuePresentKHR #240 window=0x1234\n" "[window] first Roblox Vulkan frame presented\n" "[vulkan] SDL WSI adapter shut down\n" "[main] Roblox lifecycle shutdown: Stopped\n"; std::string error; EXPECT_TRUE(ValidateReadinessLog(CanaryGraphicsBackend::kDirectVulkan, valid, &error)) << error; error.clear(); EXPECT_FALSE(ValidateReadinessLog(CanaryGraphicsBackend::kDirectVulkan, valid + "[FATAL] crash\n", &error)); const std::string missing_real_present = "[compat] legacy binary patches: disabled\n" "[compat] signal-recovery handler disabled\n" "[compat] native allocator retained; host allocator bridges disabled\n" "Loaded 2988 shaders from pack vulkan_mobile\n" "[window] first Roblox Vulkan frame presented\n" "[vulkan] SDL WSI adapter shut down\n" "[main] Roblox lifecycle shutdown: Stopped\n"; error.clear(); EXPECT_FALSE(ValidateReadinessLog(CanaryGraphicsBackend::kDirectVulkan, missing_real_present, &error)); EXPECT_NE(error.find("real queue present"), std::string::npos); } TEST(ReadinessCanaryTest, ValidatesRealOpenGlEsThreeSwap) { const std::string valid = "[compat] legacy binary patches: disabled\n" "[compat] signal-recovery handler disabled\n" "[compat] native allocator retained; host allocator bridges disabled\n" "[window] OpenGL ES context version=3.2\n" "[window] EGL context and surface initialized via SDL3\n" "[window] SwapBuffers #1 window=0x1234\n" "[window] first Roblox frame presented\n" "[main] Roblox lifecycle shutdown: Stopped\n"; std::string error; EXPECT_TRUE(ValidateReadinessLog(CanaryGraphicsBackend::kOpenGlEs, valid, &error)) << error; std::string gles2 = valid; gles2.replace(gles2.find("version=3.2"), std::string("version=3.2").size(), "version=2.0"); error.clear(); EXPECT_FALSE(ValidateReadinessLog(CanaryGraphicsBackend::kOpenGlEs, gles2, &error)); EXPECT_NE(error.find("version=3."), std::string::npos); std::string missing_swap = valid; const std::string swap_marker = "[window] SwapBuffers #1 window=0x1234\n"; missing_swap.erase(missing_swap.find(swap_marker), swap_marker.size()); error.clear(); EXPECT_FALSE(ValidateReadinessLog(CanaryGraphicsBackend::kOpenGlEs, missing_swap, &error)); EXPECT_NE(error.find("real EGL swap"), std::string::npos); } TEST(ReadinessCanaryTest, MapsWindowBackendsToTheirPresentPath) { CanaryGraphicsBackend backend = CanaryGraphicsBackend::kDirectVulkan; EXPECT_TRUE(ParseCanaryGraphicsBackend("direct-vulkan", &backend)); EXPECT_EQ(backend, CanaryGraphicsBackend::kDirectVulkan); EXPECT_TRUE(ParseCanaryGraphicsBackend("opengl", &backend)); EXPECT_EQ(backend, CanaryGraphicsBackend::kOpenGlEs); EXPECT_TRUE(ParseCanaryGraphicsBackend("angle-vulkan", &backend)); EXPECT_EQ(backend, CanaryGraphicsBackend::kAngleVulkan); EXPECT_EQ(CanaryGraphicsBackendName(backend), "angle-vulkan"); EXPECT_FALSE(ParseCanaryGraphicsBackend("unknown", &backend)); } TEST(ZipArchiveTest, ExtractsSelectedPrefixInOneSequentialPass) { TemporaryDirectory temporary; const std::filesystem::path archive = temporary.root() / "fixture.zip"; ASSERT_TRUE(CreateZip(archive)); std::size_t files = 0; std::string error; const std::filesystem::path output = temporary.root() / "output"; ASSERT_TRUE( ExtractZipPrefix(archive, "assets/", output, 1024, &files, &error)) << error; EXPECT_EQ(files, 2); EXPECT_EQ(ReadFile(output / "content/first.txt"), "first"); EXPECT_EQ(ReadFile(output / "nested/second.txt"), "second"); EXPECT_FALSE(std::filesystem::exists(output / "ignored.txt")); } std::vector CatalogFixture() { return { {"2.725.1142", 2546, "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21"}, {"2.727.1199", 2628, "1686400865ae0e408cd7bd67de7a439625c6fd13"}, {"2.734.917", 2908, "63c5109637b7d7b2bdb8ed8f858023ff5ef49326"}, }; } PayloadStoreResult InstalledFixture(std::string_view version_name, std::uint64_t version_code) { PayloadStoreResult installed; installed.payload_id = std::to_string(version_code) + "-" + std::string(40, 'a'); installed.version_name = version_name; installed.version_code = version_code; return installed; } // A rejected candidate used to end the update, leaving the user on whatever // was installed. TEST(UpdateCandidatePlanTest, FallsBackToTheNewestSupportedProfile) { const auto profiles = CatalogFixture(); const ProviderVersion latest{"2.736.1408", 2998, {}}; const PayloadStoreResult current = InstalledFixture("2.725.1142", 2546); const std::vector plans = PlanUpdateCandidates(profiles, profiles.back(), &latest, current, current); ASSERT_EQ(plans.size(), 2U); EXPECT_EQ(plans[0].identity.version_code, 2998U); EXPECT_FALSE(plans[0].exact_supported); EXPECT_EQ(plans[1].identity.version_code, 2908U); EXPECT_TRUE(plans[1].exact_supported); EXPECT_EQ(plans[1].identity.exact_build_id, "63c5109637b7d7b2bdb8ed8f858023ff5ef49326"); } TEST(UpdateCandidatePlanTest, OffersTheLatestOnceWhenItIsExactSupported) { const auto profiles = CatalogFixture(); const ProviderVersion latest{"2.734.917", 2908, {}}; const PayloadStoreResult current = InstalledFixture("2.725.1142", 2546); const std::vector plans = PlanUpdateCandidates(profiles, profiles.back(), &latest, current, current); ASSERT_EQ(plans.size(), 1U); EXPECT_EQ(plans[0].identity.version_code, 2908U); EXPECT_TRUE(plans[0].exact_supported); } TEST(UpdateCandidatePlanTest, NeverDowngradesARunnablePayload) { const auto profiles = CatalogFixture(); const ProviderVersion latest{"2.736.1408", 2998, {}}; const PayloadStoreResult current = InstalledFixture("2.736.1408", 2998); EXPECT_TRUE( PlanUpdateCandidates(profiles, profiles.back(), &latest, current, current) .empty()); // The same holds when the provider cannot be reached at all. EXPECT_TRUE(PlanUpdateCandidates(profiles, profiles.back(), nullptr, current, current) .empty()); } TEST(UpdateCandidatePlanTest, InstallsTheSupportedProfileWithoutAProvider) { const auto profiles = CatalogFixture(); const PayloadStoreResult current = InstalledFixture("2.725.1142", 2546); const std::vector plans = PlanUpdateCandidates(profiles, profiles.back(), nullptr, current, current); ASSERT_EQ(plans.size(), 1U); EXPECT_EQ(plans[0].identity.version_code, 2908U); } TEST(UpdateCandidatePlanTest, ReusesAnApprovedPayloadForTheSameVersion) { const auto profiles = CatalogFixture(); const ProviderVersion latest{"2.736.1408", 2998, {}}; PayloadStoreResult installed = InstalledFixture("2.736.1408", 2998); installed.host_abi_profile = "host_abi_profiles/2998.json"; installed.compatibility_manifest = "compatibility_profiles/2998.json"; const std::vector plans = PlanUpdateCandidates( profiles, profiles.back(), &latest, PayloadStoreResult{}, installed); ASSERT_FALSE(plans.empty()); EXPECT_EQ(plans[0].identity.version_code, 2998U); EXPECT_TRUE(plans[0].reuse_installed); } TEST(HostAbiSidecarTest, RejectsAnIdentityThatDoesNotDescribeItsOwnPayload) { TemporaryDirectory temporary; const std::string build_id = "1686400865ae0e408cd7bd67de7a439625c6fd13"; const std::filesystem::path consistent = temporary.root() / "good.json"; Write(consistent, nlohmann::json({{"schema_version", 1}, {"elf_build_id", build_id}, {"payload_id", "2628-" + build_id}, {"payload_path", "payloads/2628-" + build_id}}) .dump(2) + "\n"); const HostAbiSidecarIdentity identity = ReadHostAbiSidecarIdentity(consistent); ASSERT_TRUE(identity) << identity.error; EXPECT_EQ(identity.elf_build_id, build_id); EXPECT_EQ(identity.payload_id, "2628-" + build_id); const std::filesystem::path mismatched = temporary.root() / "bad.json"; const std::string other = "2908-" + std::string(40, 'b'); Write(mismatched, nlohmann::json({{"schema_version", 1}, {"elf_build_id", build_id}, {"payload_id", other}, {"payload_path", "payloads/" + other}}) .dump(2) + "\n"); EXPECT_FALSE(ReadHostAbiSidecarIdentity(mismatched)); } // The regression: the library used to be the newest supported profile while // the sidecar stayed whatever file shipped, and the pair failed every // derivation. TEST(ReferenceProfileTest, FollowsTheSidecarRatherThanTheNewestProfile) { TemporaryDirectory temporary; const auto profiles = CatalogFixture(); const auto preferred = PreferredSupportedProfile(profiles); ASSERT_TRUE(preferred.has_value()); const auto& older = profiles[1]; const std::string payload_id = std::to_string(older.version_code) + "-" + older.elf_build_id; const auto reference_file = temporary.root() / "reference.json"; Write(reference_file, nlohmann::json({{"schema_version", 1}, {"elf_build_id", older.elf_build_id}, {"payload_id", payload_id}, {"payload_path", "payloads/" + payload_id}}) .dump(2) + "\n"); std::filesystem::path sidecar; const auto reference = ResolveReferenceProfile(reference_file, profiles, &sidecar); ASSERT_TRUE(reference.has_value()); EXPECT_EQ(sidecar, reference_file); EXPECT_EQ(reference->elf_build_id, ReadHostAbiSidecarIdentity(sidecar).elf_build_id); EXPECT_EQ(reference->elf_build_id, older.elf_build_id); EXPECT_NE(reference->elf_build_id, preferred->elf_build_id); } TEST(ReferenceProfileTest, PicksTheNewestSidecarInADirectory) { TemporaryDirectory temporary; const std::filesystem::path directory = temporary.root() / "host_abi"; const auto profiles = CatalogFixture(); for (const SupportedPayloadProfile& profile : {profiles[1], profiles[2]}) { const std::string payload_id = std::to_string(profile.version_code) + "-" + profile.elf_build_id; Write(directory / (profile.elf_build_id + ".json"), nlohmann::json({{"schema_version", 1}, {"elf_build_id", profile.elf_build_id}, {"payload_id", payload_id}, {"payload_path", "payloads/" + payload_id}}) .dump(2) + "\n"); } std::filesystem::path sidecar; const auto reference = ResolveReferenceProfile(directory, profiles, &sidecar); ASSERT_TRUE(reference.has_value()); EXPECT_EQ(reference->version_code, 2908U); EXPECT_EQ(sidecar, directory / (profiles[2].elf_build_id + ".json")); // A sidecar for a Build ID no supported profile owns is not a reference. const auto orphaned = ResolveReferenceProfile( directory, {{"2.740.0", 3100, std::string(40, 'c')}}, &sidecar); EXPECT_FALSE(orphaned.has_value()); } // A sidecar for a Build ID the catalog does not carry disables Tier C. TEST(ShippedMetadataTest, ReferenceSidecarDescribesASupportedProfile) { const std::filesystem::path root = MOCKTAIL_TEST_SOURCE_DIR; const CompatibilityCatalogResult catalog = LoadCompatibilityCatalog(root / "config/roblox_compatibility.json"); ASSERT_TRUE(catalog) << catalog.error; const HostAbiSidecarIdentity identity = ReadHostAbiSidecarIdentity( root / "config/roblox_host_abi_reference.json"); ASSERT_TRUE(identity) << identity.error; std::string sidecar_abi = "x86_64"; for (const auto& entry : nlohmann::json::parse( ReadFile(root / "config/roblox_compatibility.json")) .at("profiles")) { if (entry.value("elf_build_id", "") == identity.elf_build_id) { sidecar_abi = entry.value("abi", "x86_64"); } } if (sidecar_abi != compat::kGuestAbi) { GTEST_SKIP() << "shipped reference sidecar targets " << sidecar_abi; } const auto supported = std::find_if( catalog.profiles.begin(), catalog.profiles.end(), [&](const SupportedPayloadProfile& profile) { return profile.elf_build_id == identity.elf_build_id; }); ASSERT_NE(supported, catalog.profiles.end()) << "no supported profile owns reference Build ID " << identity.elf_build_id; EXPECT_EQ(identity.payload_id, std::to_string(supported->version_code) + "-" + supported->elf_build_id); // A stale reference can derive a runnable client while silently dropping // fullscreen synchronization and the host audio output menu. const auto preferred = PreferredSupportedProfile(catalog.profiles); ASSERT_TRUE(preferred.has_value()); EXPECT_EQ(identity.elf_build_id, preferred->elf_build_id); const auto runtime = compat::FindBuildProfile( (root / "config/roblox_compatibility.json").string(), identity.elf_build_id); ASSERT_TRUE(runtime) << runtime.error; ASSERT_TRUE(runtime.profile.has_value()); EXPECT_TRUE( runtime.profile->user_game_settings_fullscreen_setter_rva.has_value()); 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")); if (payload.at("abi").get_ref() != compat::kGuestAbi) { EXPECT_NE(compat::FindHostAbiProfile(preferred->elf_build_id), nullptr); GTEST_SKIP() << "shipped default payload targets " << payload.at("abi").get(); } 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}, {"payload_path", "payloads/" + payload_id}}) .dump(2) + "\n"; } // Both publication entry points must preserve the original rollback manifest. class PayloadStorePromotionTest : public ::testing::TestWithParam { protected: void SetUp() override { root = temporary.root() / "store"; compatibility = temporary.root() / "compatibility.json"; profile = temporary.root() / "candidate.json"; candidate_compatibility = temporary.root() / "candidate-compatibility.json"; const auto prepared = temporary.root() / "prepared"; Write(prepared / "libroblox.so", "native-library"); Write(prepared / "sober_apk/base.apk", "base-apk"); Write(prepared / "sober_apk" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; const auto asset_hash = HashAssetTree(prepared / "assets", &asset_count, &error); ASSERT_TRUE(error.empty()) << error; const std::string build_id = GetParam() ? "a6c1f5c57f9d7aa3fb99a5fa30565e07e5c88f6a" : "1686400865ae0e408cd7bd67de7a439625c6fd13"; const auto library_hash = HashRegularFile(prepared / "libroblox.so"); const nlohmann::json metadata = { {"schema_version", 1}, {"package", "com.roblox.client"}, {"version_name", "2.727.1199"}, {"version_code", 2628}, {"abi", std::string(compat::kGuestAbi)}, {"elf_build_id", build_id}, {"sha256", {{"libroblox", library_hash}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.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"); Write(compatibility, "{\"schema_version\":1,\"profiles\":[{" "\"version_name\":\"2.727.1199\",\"version_code\":2628," "\"elf_build_id\":\"1686400865ae0e408cd7bd67de7a439625c6fd13\"," "\"status\":\"supported\",\"default_allowed\":true," "\"allow_legacy_binary_patches\":false,\"abi\":\"" + std::string(compat::kGuestAbi) + "\"}]}\n"); runtime = std::filesystem::canonical("/proc/self/exe"); PayloadStore store(root, compatibility, runtime); const auto staged = store.Stage(prepared); ASSERT_TRUE(staged) << staged.error; candidate_id = staged.payload_id; Write(profile, nlohmann::json( {{"schema_version", 1}, {"elf_build_id", build_id}, {"payload_sha256", library_hash}, {"payload_id", candidate_id}, {"payload_path", "payloads/" + candidate_id}, {"reference", {{"elf_build_id", "1686400865ae0e408cd7bd67de7a439625c6fd13"}, {"payload_sha256", "3e9c26c81186f93458ff65d8a8bc240c220974db02b8388b91340b77b336" "997d"}}}, {"profile", {{"elf_build_id", build_id}}}, {"derivation_anchors", {{"signature_version", 1}}}}) .dump(2) + "\n"); Write(candidate_compatibility, nlohmann::json( {{"schema_version", 1}, {"profiles", nlohmann::json::array( {{{"version_name", "2.727.1199"}, {"version_code", 2628}, {"elf_build_id", build_id}, {"status", "experimental"}, {"default_allowed", true}, {"allow_legacy_binary_patches", false}, {"allow_host_abi_bridges", true}, {"allow_host_constructor_replay", true}}})}}) .dump(2) + "\n"); logs = {temporary.root() / "canary-1.log", temporary.root() / "canary-2.log"}; Write(logs[0], "first independent Tier C pass\n"); Write(logs[1], "second independent Tier C pass\n"); } PayloadStoreResult Promote(ManifestWriteFault fault = {}) { PayloadStore store(root, compatibility, runtime, std::move(fault)); return GetParam() ? store.PromoteProbation(candidate_id, profile, candidate_compatibility, logs) : store.Promote(candidate_id); } void ResetManifests(bool previous_present = true) { Write(root / "current.json", current_bytes); std::filesystem::remove(root / "previous_good.json"); if (previous_present) Write(root / "previous_good.json", previous_bytes); } void ExpectManifests(bool previous_present = true) { EXPECT_EQ(ReadFile(root / "current.json"), current_bytes); EXPECT_EQ(std::filesystem::exists(root / "previous_good.json"), previous_present); if (previous_present) EXPECT_EQ(ReadFile(root / "previous_good.json"), previous_bytes); EXPECT_FALSE( std::filesystem::exists(root / ".previous_good-recovery.json")); EXPECT_FALSE( std::filesystem::exists(root / ".previous_good-recovery.absent")); for (const auto& entry : std::filesystem::directory_iterator(root)) { EXPECT_NE(entry.path().filename().string().find(".manifest-"), 0U); } } TemporaryDirectory temporary; std::filesystem::path root, compatibility, runtime, profile, candidate_compatibility; std::array logs; std::string candidate_id; const std::string previous_id = "2908-" + std::string(40, 'b'); const std::string current_bytes = ActivationManifestFixture("2998-" + std::string(40, 'a')); std::string previous_bytes = "\n\t" + ActivationManifestFixture(previous_id) + " \n"; const std::array phases = { ManifestWritePhase::kCreate, ManifestWritePhase::kWrite, ManifestWritePhase::kFileFsync, ManifestWritePhase::kRename}; }; TEST_P(PayloadStorePromotionTest, CurrentWriteFailureRestoresPrevious) { for (const auto phase : phases) { for (const bool present : {true, false}) { SCOPED_TRACE(static_cast(phase)); SCOPED_TRACE(present); ResetManifests(present); int faults = 0; const auto result = Promote([&](const auto& path, auto at) { if (path.filename() != "current.json" || at != phase) return false; ++faults; return true; }); EXPECT_FALSE(result); EXPECT_EQ(faults, 1); ExpectManifests(present); } } } TEST_P(PayloadStorePromotionTest, FirstWriteFailurePreservesBothManifests) { for (const auto phase : phases) { for (const bool present : {true, false}) { SCOPED_TRACE(static_cast(phase)); SCOPED_TRACE(present); ResetManifests(present); int faults = 0; const auto result = Promote([&](const auto& path, auto at) { if (path.filename() != "previous_good.json" || at != phase) return false; ++faults; return true; }); EXPECT_FALSE(result); EXPECT_EQ(faults, 1); ExpectManifests(present); } } } TEST_P(PayloadStorePromotionTest, SuccessfulPublicationKeepsOldCurrent) { for (const bool present : {true, false}) { ResetManifests(present); const auto result = Promote(); ASSERT_TRUE(result) << result.error; EXPECT_EQ( nlohmann::json::parse(ReadFile(root / "current.json"))["payload_id"], candidate_id); EXPECT_EQ(ReadFile(root / "previous_good.json"), current_bytes); EXPECT_FALSE( std::filesystem::exists(root / ".previous_good-recovery.json")); EXPECT_FALSE( std::filesystem::exists(root / ".previous_good-recovery.absent")); } } TEST_P(PayloadStorePromotionTest, FailedRestorationRetainsBytesAndBlocksGarbageCollection) { for (const auto phase : phases) { SCOPED_TRACE(static_cast(phase)); ResetManifests(); Write(root / "payloads" / previous_id / "libroblox.so", "last fallback"); Write(root / "quarantine/collision/libroblox.so", "quarantined"); int previous_writes = 0; const auto result = Promote([&](const auto& path, auto at) { if (path.filename() == "current.json" && at == ManifestWritePhase::kCreate) return true; return path.filename() == "previous_good.json" && at == phase && ++previous_writes == 2; }); EXPECT_FALSE(result); EXPECT_EQ(previous_writes, 2); EXPECT_NE(result.error.find("cannot create atomic payload manifest"), std::string::npos); EXPECT_NE(result.error.find("previous_good restoration failed:"), std::string::npos); EXPECT_NE(result.error.find("recovery retained at"), std::string::npos); EXPECT_EQ(ReadFile(root / "current.json"), current_bytes); EXPECT_EQ(ReadFile(root / "previous_good.json"), current_bytes); EXPECT_EQ(ReadFile(root / ".previous_good-recovery.json"), previous_bytes); PayloadStore store(root, compatibility); const auto garbage = store.CollectGarbage({}); EXPECT_FALSE(garbage); EXPECT_TRUE(garbage.removed.empty()); EXPECT_EQ(garbage.freed_bytes, 0U); EXPECT_TRUE(std::filesystem::exists(root / "payloads" / previous_id)); EXPECT_TRUE( std::filesystem::exists(root / "quarantine/collision/libroblox.so")); EXPECT_FALSE(Promote()); EXPECT_EQ(ReadFile(root / ".previous_good-recovery.json"), previous_bytes); std::filesystem::remove(root / ".previous_good-recovery.json"); } } TEST_P(PayloadStorePromotionTest, FailedAbsenceRestorationRetainsMarker) { ResetManifests(false); const auto result = Promote([&](const auto& path, auto phase) { if (path.filename() != "current.json" || phase != ManifestWritePhase::kCreate) return false; std::filesystem::remove(root / "previous_good.json"); Write(root / "previous_good.json/blocked", "cannot remove nonempty directory"); return true; }); EXPECT_FALSE(result); EXPECT_NE(result.error.find("cannot create atomic payload manifest"), std::string::npos); EXPECT_NE(result.error.find("cannot restore absence of previous_good"), std::string::npos); EXPECT_EQ(ReadFile(root / "current.json"), current_bytes); EXPECT_EQ(ReadFile(root / ".previous_good-recovery.absent"), "absent\n"); PayloadStore store(root, compatibility); EXPECT_FALSE(store.CollectGarbage({})); EXPECT_TRUE(std::filesystem::exists(root / "payloads" / candidate_id)); EXPECT_FALSE(Promote()); EXPECT_EQ(ReadFile(root / ".previous_good-recovery.absent"), "absent\n"); } TEST_P(PayloadStorePromotionTest, RestoresExplicitlyPresentEmptyPrevious) { previous_bytes.clear(); ResetManifests(); const auto result = Promote([](const auto& path, auto phase) { return path.filename() == "current.json" && phase == ManifestWritePhase::kRename; }); EXPECT_FALSE(result); ExpectManifests(); } TEST_P(PayloadStorePromotionTest, SnapshotFailureDoesNotChangeManifests) { for (const auto phase : phases) { for (const bool present : {true, false}) { SCOPED_TRACE(static_cast(phase)); SCOPED_TRACE(present); ResetManifests(present); int faults = 0; const auto result = Promote([&](const auto& path, auto at) { if (path.filename() != (present ? ".previous_good-recovery.json" : ".previous_good-recovery.absent") || at != phase) return false; ++faults; return true; }); EXPECT_FALSE(result); EXPECT_EQ(faults, 1); ExpectManifests(present); } } ResetManifests(); std::filesystem::remove(root / "previous_good.json"); std::filesystem::create_directory(root / "previous_good.json"); EXPECT_FALSE(Promote()); EXPECT_EQ(ReadFile(root / "current.json"), current_bytes); EXPECT_TRUE(std::filesystem::is_directory(root / "previous_good.json")); EXPECT_FALSE(std::filesystem::exists(root / ".previous_good-recovery.json")); } TEST_P(PayloadStorePromotionTest, PendingRecoveryIncludingSymlinksBlocksPublicationAndCollection) { ResetManifests(); Write(root / "payloads" / previous_id / "libroblox.so", "last fallback"); for (const char* name : {".previous_good-recovery.json", ".previous_good-recovery.absent"}) { for (const bool symlink : {false, true}) { SCOPED_TRACE(name); SCOPED_TRACE(symlink); const auto recovery = root / name; if (symlink) std::filesystem::create_symlink("missing-target", recovery); else Write(recovery, "pending recovery bytes"); const auto result = Promote(); EXPECT_FALSE(result); EXPECT_NE(result.error.find("recovery is pending"), std::string::npos); PayloadStore store(root, compatibility); EXPECT_FALSE(store.CollectGarbage({})); EXPECT_EQ(ReadFile(root / "current.json"), current_bytes); EXPECT_EQ(ReadFile(root / "previous_good.json"), previous_bytes); EXPECT_TRUE(std::filesystem::exists(root / "payloads" / previous_id)); if (symlink) EXPECT_TRUE(std::filesystem::is_symlink(recovery)); else EXPECT_EQ(ReadFile(recovery), "pending recovery bytes"); std::filesystem::remove(recovery); } } } TEST_P(PayloadStorePromotionTest, NoPreviousExchangeForFirstInstallOrSamePayload) { for (const bool same_payload : {false, true}) { for (const bool fail_current : {false, true}) { for (const bool present : {false, true}) { ResetManifests(present); if (same_payload) Write(root / "current.json", ActivationManifestFixture(candidate_id)); else std::filesystem::remove(root / "current.json"); const auto before = ReadFile(root / "current.json"); int other_writes = 0; const auto result = Promote([&](const auto& path, auto phase) { if (path.filename() != "current.json") ++other_writes; return fail_current && path.filename() == "current.json" && phase == ManifestWritePhase::kCreate; }); EXPECT_EQ(static_cast(result), !fail_current); EXPECT_EQ(other_writes, 0); EXPECT_EQ(std::filesystem::exists(root / "previous_good.json"), present); if (present) EXPECT_EQ(ReadFile(root / "previous_good.json"), previous_bytes); if (fail_current) { EXPECT_EQ(ReadFile(root / "current.json"), before); EXPECT_EQ(std::filesystem::exists(root / "current.json"), same_payload); } } } } } TEST_P(PayloadStorePromotionTest, CleanupFailureReportsWhetherCurrentWasPublished) { for (const bool publish : {false, true}) { ResetManifests(); const auto recovery = root / ".previous_good-recovery.json"; const auto result = Promote([&](const auto& path, auto phase) { if (path.filename() != "current.json" || phase != ManifestWritePhase::kCreate) return false; // Preserve the snapshot in a nonempty directory to make unlink fail. const auto bytes = ReadFile(recovery); std::filesystem::remove(recovery); Write(recovery / "original", bytes); return !publish; }); EXPECT_FALSE(result); EXPECT_NE(result.error.find("cannot remove previous_good recovery"), std::string::npos); EXPECT_EQ( result.error.find("current manifest published") != std::string::npos, publish); EXPECT_EQ(ReadFile(recovery / "original"), previous_bytes); if (publish) { EXPECT_EQ( nlohmann::json::parse(ReadFile(root / "current.json"))["payload_id"], candidate_id); EXPECT_EQ(ReadFile(root / "previous_good.json"), current_bytes); } else { EXPECT_EQ(ReadFile(root / "current.json"), current_bytes); EXPECT_EQ(ReadFile(root / "previous_good.json"), previous_bytes); } PayloadStore store(root, compatibility); EXPECT_FALSE(store.CollectGarbage({})); EXPECT_FALSE(Promote()); std::filesystem::remove_all(recovery); } } TEST_P(PayloadStorePromotionTest, JsonManifestSchemaTypeReturnsError) { Write(root / "current.json", "{\"schema_version\":\"1\"}\n"); ASSERT_EXIT( { PayloadStore store(root, compatibility); const auto result = store.InspectCurrent(); std::_Exit(!result && !result.error.empty() ? 0 : 10); }, ::testing::ExitedWithCode(0), ""); } TEST_P(PayloadStorePromotionTest, JsonIntegrityNestedHashTypeReturnsError) { const auto prepared = temporary.root() / "prepared"; auto metadata = nlohmann::json::parse(ReadFile(prepared / "roblox_payload.json")); metadata["sha256"]["libroblox"] = false; Write(prepared / "roblox_payload.json", metadata.dump()); ASSERT_EXIT( { const auto result = InspectPreparedPayload(prepared); std::_Exit(!result && !result.error.empty() ? 0 : 10); }, ::testing::ExitedWithCode(0), ""); } TEST_P(PayloadStorePromotionTest, JsonCatalogFlagTypeReturnsError) { auto catalog = nlohmann::json::parse(ReadFile(compatibility)); catalog["profiles"][0]["default_allowed"] = "true"; Write(compatibility, catalog.dump()); ASSERT_EXIT( { const auto result = LoadCompatibilityCatalog(compatibility); std::_Exit(!result && !result.error.empty() ? 0 : 10); }, ::testing::ExitedWithCode(0), ""); } std::string JsonWithOverflowNumber(nlohmann::json document, const nlohmann::json::json_pointer& field) { document[field] = "overflow-number"; std::string contents = document.dump(); const auto offset = contents.find("\"overflow-number\""); contents.replace(offset, std::string("\"overflow-number\"").size(), "18446744073709551616"); return contents; } TEST_P(PayloadStorePromotionTest, JsonManifestRejectsInvalidFields) { ASSERT_TRUE(Promote()); const auto valid = nlohmann::json::parse(ReadFile(root / "current.json")); std::vector cases = {"null", "[]", "true", "1"}; for (const char* field : {"schema_version", "payload_id", "payload_path", "version_name", "version_code", "elf_build_id", "host_abi_profile_path", "compatibility_manifest_path", "approval_path", "payload_sha256"}) { for (const auto& bad : nlohmann::json::array({nullptr, true, 42, nlohmann::json::object(), nlohmann::json::array()})) { auto document = valid; document[field] = bad; cases.push_back(document.dump()); } if (std::string_view(field).find("_path") == std::string_view::npos || std::string_view(field) == "payload_path") { if (std::string_view(field) != "payload_sha256") { auto document = valid; document.erase(field); cases.push_back(document.dump()); } } } for (const auto& bad : nlohmann::json::array({"1", 0, -1, 1.0, 4294967297ULL})) { for (const char* field : {"schema_version", "version_code"}) { auto document = valid; document[field] = bad; cases.push_back(document.dump()); } } cases.push_back(JsonWithOverflowNumber( valid, nlohmann::json::json_pointer("/version_code"))); cases.push_back(JsonWithOverflowNumber( valid, nlohmann::json::json_pointer("/schema_version"))); for (const auto& contents : cases) { SCOPED_TRACE(contents); Write(root / "current.json", contents); ASSERT_EXIT( { PayloadStore store(root, compatibility); const auto inspected = store.InspectCurrent(); const auto verified = store.VerifyCurrent(); std::_Exit(!inspected && !inspected.error.empty() && !verified && !verified.error.empty() ? 0 : 10); }, ::testing::ExitedWithCode(0), ""); } } TEST_P(PayloadStorePromotionTest, JsonIntegrityRejectsInvalidFields) { const auto prepared = temporary.root() / "prepared"; const auto valid = nlohmann::json::parse(ReadFile(prepared / "roblox_payload.json")); std::vector cases = {"null", "[]", "true", "1"}; const std::vector fields = { "/schema_version", "/package", "/version_name", "/version_code", "/elf_build_id", "/sha256", "/assets", "/sha256/libroblox", "/sha256/base_apk", "/sha256/" + std::string(compat::kGuestSplitApkHashKey), "/assets/file_count", "/assets/sha256_tree"}; for (const auto& field : fields) { const nlohmann::json::json_pointer pointer(field); auto missing = valid; const auto slash = field.find_last_of('/'); missing[nlohmann::json::json_pointer(field.substr(0, slash))].erase( field.substr(slash + 1)); cases.push_back(missing.dump()); for (const auto& bad : nlohmann::json::array( {nullptr, true, -1, 1.5, nlohmann::json::array()})) { auto document = valid; document[pointer] = bad; cases.push_back(document.dump()); } } for (const char* field : {"/schema_version", "/version_code", "/assets/file_count"}) { cases.push_back( JsonWithOverflowNumber(valid, nlohmann::json::json_pointer(field))); } for (const char* field : {"/schema_version", "/version_code", "/assets/file_count"}) { auto string_number = valid; string_number[nlohmann::json::json_pointer(field)] = "1"; cases.push_back(string_number.dump()); } auto zero_version = valid; zero_version["version_code"] = 0; cases.push_back(zero_version.dump()); auto wrapped_schema = valid; wrapped_schema["schema_version"] = 4294967297ULL; cases.push_back(wrapped_schema.dump()); if (std::numeric_limits::max() < std::numeric_limits::max()) { auto too_many = valid; too_many["assets"]["file_count"] = static_cast(std::numeric_limits::max()) + 1; cases.push_back(too_many.dump()); } for (const auto& contents : cases) { SCOPED_TRACE(contents); Write(prepared / "roblox_payload.json", contents); ASSERT_EXIT( { const auto inspected = InspectPreparedPayload(prepared); const auto verified = VerifyPreparedPayload(prepared); PayloadStore store(root, compatibility); const auto staged = store.Stage(prepared); std::_Exit(!inspected && !inspected.error.empty() && !verified && !verified.error.empty() && !staged && !staged.error.empty() ? 0 : 10); }, ::testing::ExitedWithCode(0), ""); } } TEST_P(PayloadStorePromotionTest, JsonCatalogRejectsInvalidFields) { const auto valid = nlohmann::json::parse(ReadFile(compatibility)); std::vector cases = {"null", "[]", "true", "1"}; for (const char* field : {"schema_version", "profiles"}) { auto missing = valid; missing.erase(field); cases.push_back(missing.dump()); for (const auto& bad : nlohmann::json::array( {nullptr, true, "wrong", 1.5, nlohmann::json::object()})) { auto document = valid; document[field] = bad; cases.push_back(document.dump()); } } for (const char* field : {"status", "default_allowed", "allow_legacy_binary_patches", "version_name", "version_code", "elf_build_id", "abi"}) { for (const auto& bad : nlohmann::json::array({nullptr, 1.5, nlohmann::json::object(), nlohmann::json::array()})) { auto document = valid; document["profiles"][0][field] = bad; cases.push_back(document.dump()); } if (std::string_view(field) == "version_name" || std::string_view(field) == "version_code" || std::string_view(field) == "elf_build_id") { auto missing = valid; missing["profiles"][0].erase(field); cases.push_back(missing.dump()); } } for (const char* field : {"default_allowed", "allow_legacy_binary_patches"}) { for (const auto& bad : nlohmann::json::array({"true", 1})) { auto document = valid; document["profiles"][0][field] = bad; cases.push_back(document.dump()); } } for (const auto& bad : nlohmann::json::array({-1, "2628", 0})) { auto document = valid; document["profiles"][0]["version_code"] = bad; cases.push_back(document.dump()); } cases.push_back(JsonWithOverflowNumber( valid, nlohmann::json::json_pointer("/profiles/0/version_code"))); cases.push_back(JsonWithOverflowNumber( valid, nlohmann::json::json_pointer("/schema_version"))); auto wrapped_schema = valid; wrapped_schema["schema_version"] = 4294967297ULL; cases.push_back(wrapped_schema.dump()); for (const auto& contents : cases) { SCOPED_TRACE(contents); Write(compatibility, contents); ASSERT_EXIT( { const auto result = LoadCompatibilityCatalog(compatibility); std::_Exit(!result && !result.error.empty() ? 0 : 10); }, ::testing::ExitedWithCode(0), ""); } } TEST_P(PayloadStorePromotionTest, JsonOptionalDefaultsAndNumericLimitsStayValid) { ASSERT_TRUE(Promote()); const auto activation = nlohmann::json::parse(ReadFile(root / "current.json")); PayloadStore store(root, compatibility); for (const bool absent : {false, true}) { auto document = activation; for (const char* field : {"host_abi_profile_path", "compatibility_manifest_path", "approval_path"}) { if (absent) document.erase(field); else document[field] = ""; } document.erase("payload_sha256"); Write(root / "current.json", document.dump()); ASSERT_TRUE(store.InspectCurrent()); } Write(root / "current.json", activation.dump()); ASSERT_TRUE(store.VerifyCurrent()); const auto prepared = temporary.root() / "prepared"; auto metadata = nlohmann::json::parse(ReadFile(prepared / "roblox_payload.json")); metadata["version_code"] = std::numeric_limits::max(); metadata["assets"]["file_count"] = 0; Write(prepared / "roblox_payload.json", metadata.dump()); auto inspected = InspectPreparedPayload(prepared); ASSERT_TRUE(inspected) << inspected.error; EXPECT_EQ(inspected.metadata.version_code, std::numeric_limits::max()); EXPECT_EQ(inspected.metadata.asset_file_count, 0U); metadata["assets"]["file_count"] = std::numeric_limits::max(); Write(prepared / "roblox_payload.json", metadata.dump()); inspected = InspectPreparedPayload(prepared); ASSERT_TRUE(inspected) << inspected.error; EXPECT_EQ(inspected.metadata.asset_file_count, std::numeric_limits::max()); const auto catalog = nlohmann::json::parse(ReadFile(compatibility)); for (const char* field : {"status", "default_allowed", "allow_legacy_binary_patches", "abi"}) { auto document = catalog; auto extra = catalog["profiles"][0]; extra.erase(field); document["profiles"].push_back(extra); Write(compatibility, document.dump()); const auto result = LoadCompatibilityCatalog(compatibility); ASSERT_TRUE(result) << result.error; // A profile without abi means x86_64, so only an x86_64 host counts it. const bool defaults_to_host = std::string_view(field) == "abi" && compat::kGuestAbi == "x86_64"; EXPECT_EQ(result.profiles.size(), defaults_to_host ? 2U : 1U); } auto document = catalog; document["profiles"][0]["version_code"] = std::numeric_limits::max(); auto foreign = document["profiles"][0]; foreign["abi"] = "foreign-abi"; document["profiles"].push_back(foreign); document["profiles"].push_back(nullptr); document["profiles"].push_back(nlohmann::json::array()); Write(compatibility, document.dump()); const auto result = LoadCompatibilityCatalog(compatibility); ASSERT_TRUE(result) << result.error; ASSERT_EQ(result.profiles.size(), 1U); EXPECT_EQ(result.profiles[0].version_code, std::numeric_limits::max()); } INSTANTIATE_TEST_SUITE_P(Publication, PayloadStorePromotionTest, ::testing::Values(false, true)); class UpdateCoordinatorTest : public PayloadStorePromotionTest { protected: void SetUp() override { PayloadStorePromotionTest::SetUp(); ASSERT_FALSE(GetParam()); ASSERT_TRUE(Promote()); active_id = candidate_id; const auto prepared = temporary.root() / "newer-prepared"; std::filesystem::copy(temporary.root() / "prepared", prepared, std::filesystem::copy_options::recursive); Write(prepared / "libroblox.so", "newer-native-library"); auto metadata = nlohmann::json::parse(ReadFile(prepared / "roblox_payload.json")); metadata["version_name"] = "2.734.917"; metadata["version_code"] = 2908; metadata["elf_build_id"] = std::string(40, 'b'); metadata["sha256"]["libroblox"] = HashRegularFile(prepared / "libroblox.so"); Write(prepared / "roblox_payload.json", metadata.dump()); auto catalog = nlohmann::json::parse(ReadFile(compatibility)); auto newer = catalog["profiles"][0]; newer["version_name"] = metadata["version_name"]; newer["version_code"] = metadata["version_code"]; newer["elf_build_id"] = metadata["elf_build_id"]; catalog["profiles"].push_back(newer); Write(compatibility, catalog.dump()); PayloadStore store(root, compatibility, runtime); const auto staged = store.Stage(prepared); ASSERT_TRUE(staged) << staged.error; newer_id = staged.payload_id; ASSERT_TRUE(store.VerifyCurrent()); const auto identity = compat::ReadElfBuildId(runtime.string()); ASSERT_TRUE(identity) << identity.error; paths.config_file = temporary.root() / "config.yaml"; Write(paths.config_file, "version: 1\nupdates:\n automatic: true\n source: apk-pure\n"); paths.data_root = root; paths.cache_root = temporary.root() / "cache"; paths.state_root = temporary.root() / "state"; paths.compatibility_manifest = compatibility; paths.runtime_binary = runtime; request.startup_preflight = true; request.check_latest = false; request.run_canary = true; script = temporary.root() / "canary.sh"; WriteScript(); } void WriteScript(bool accepted = true) { Write( script, "#!/bin/sh\ncat <<'READY'\n" "[compat] legacy binary patches: disabled\n" "[compat] signal-recovery handler disabled\n" "[compat] native allocator retained; host allocator bridges disabled\n" "[window] vkQueuePresentKHR #240 window=0x1234\n" "[window] first Roblox Vulkan frame presented\n" "[vulkan] SDL WSI adapter shut down\n" "[window] OpenGL ES context version=3.2\n" "[window] EGL context and surface initialized via SDL3\n" "[window] SwapBuffers #1 window=0x1234\n" "[window] first Roblox frame presented\n" "[main] Roblox lifecycle shutdown: Stopped\nREADY\n" + std::string(accepted ? "" : "echo '[FATAL] rejected graphics'\n")); ASSERT_EQ(chmod(script.c_str(), 0700), 0); } CanarySpawn Spawn(int initial_error = 0) { return [this, initial_error](pid_t* child, const char*, const posix_spawn_file_actions_t* actions, const posix_spawnattr_t* attributes, char* const arguments[], char* const environment[]) { ++spawns; if (spawns == 1 && initial_error != 0) return initial_error; return posix_spawn(child, script.c_str(), actions, attributes, arguments, environment); }; } std::filesystem::path Marker() const { const auto identity = compat::ReadElfBuildId(paths.runtime_binary.string()); EXPECT_TRUE(identity) << identity.error; return root / "rejections" / (newer_id + "-" + identity.build_id + "-" + std::string( CanaryGraphicsBackendName(request.canary_graphics_backend)) + ".txt"); } std::string CurrentId() const { PayloadStore store(root, compatibility, paths.runtime_binary); const auto current = store.VerifyCurrent(); EXPECT_TRUE(current) << current.error; return current.payload_id; } void RejectOnce() { WriteScript(false); request.canary_spawn = Spawn(); const auto result = RunUpdate(paths, request); ASSERT_TRUE(result) << result.error; EXPECT_FALSE(result.changed); EXPECT_EQ(result.payload_id, active_id); EXPECT_EQ(CurrentId(), active_id); EXPECT_EQ(spawns, 1); EXPECT_TRUE(std::filesystem::exists(Marker())); WriteScript(); } CanaryOptions Options() const { CanaryOptions options; options.runtime_binary = paths.runtime_binary; options.payload_directory = root / "payloads" / newer_id; options.compatibility_manifest = compatibility; options.cache_root = paths.cache_root; options.state_root = paths.state_root; options.timeout_seconds = 2; return options; } UpdatePaths paths; UpdateRequest request; std::filesystem::path script; std::string active_id, newer_id; int spawns = 0; }; TEST_P(UpdateCoordinatorTest, RetriesTheSameCandidateOnNextStartupAfterEagain) { request.canary_spawn = Spawn(EAGAIN); const auto first = RunUpdate(paths, request); ASSERT_TRUE(first) << first.error; EXPECT_FALSE(first.changed); EXPECT_EQ(first.payload_id, active_id); EXPECT_EQ(CurrentId(), active_id); EXPECT_EQ(spawns, 1); EXPECT_FALSE(std::filesystem::exists(Marker())); const auto second = RunUpdate(paths, request); ASSERT_TRUE(second) << second.error; EXPECT_TRUE(second.changed); EXPECT_EQ(second.payload_id, newer_id); EXPECT_EQ(CurrentId(), newer_id); EXPECT_EQ(spawns, 2); EXPECT_FALSE(std::filesystem::exists(Marker())); } TEST_P(UpdateCoordinatorTest, RealRejectionIsFilteredOnNextStartup) { RejectOnce(); const auto second = RunUpdate(paths, request); ASSERT_TRUE(second) << second.error; EXPECT_FALSE(second.changed); EXPECT_EQ(second.payload_id, active_id); EXPECT_EQ(CurrentId(), active_id); EXPECT_EQ(spawns, 1); EXPECT_TRUE(std::filesystem::exists(Marker())); EXPECT_NE(second.message.find("already failed"), std::string::npos); } TEST_P(UpdateCoordinatorTest, DifferentBackendRetriesRejectedCandidate) { RejectOnce(); const auto old_marker = Marker(); request.canary_graphics_backend = CanaryGraphicsBackend::kOpenGlEs; EXPECT_FALSE(std::filesystem::exists(Marker())); const auto result = RunUpdate(paths, request); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.changed); EXPECT_EQ(CurrentId(), newer_id); EXPECT_EQ(spawns, 2); EXPECT_TRUE(std::filesystem::exists(old_marker)); EXPECT_FALSE(std::filesystem::exists(Marker())); } TEST_P(UpdateCoordinatorTest, DifferentRuntimeBuildIdRetriesRejectedCandidate) { RejectOnce(); const auto old_marker = Marker(); const auto original = compat::ReadElfBuildId(paths.runtime_binary.string()); const auto different = std::filesystem::canonical("/usr/bin/true"); const auto identity = compat::ReadElfBuildId(different.string()); ASSERT_TRUE(original) << original.error; ASSERT_TRUE(identity) << identity.error; ASSERT_NE(identity.build_id, original.build_id); paths.runtime_binary = different; EXPECT_FALSE(std::filesystem::exists(Marker())); const auto result = RunUpdate(paths, request); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.changed); EXPECT_EQ(CurrentId(), newer_id); EXPECT_EQ(spawns, 2); EXPECT_TRUE(std::filesystem::exists(old_marker)); EXPECT_FALSE(std::filesystem::exists(Marker())); } TEST_P(UpdateCoordinatorTest, ExplicitUpdateCanRetryARejectedCandidate) { RejectOnce(); request.startup_preflight = false; const auto result = RunUpdate(paths, request); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.changed); EXPECT_EQ(result.payload_id, newer_id); EXPECT_EQ(CurrentId(), newer_id); EXPECT_EQ(spawns, 2); } TEST_P(UpdateCoordinatorTest, FirstInstallCanRetryARejectedCandidate) { RejectOnce(); ASSERT_TRUE(std::filesystem::remove(root / "current.json")); const auto result = RunUpdate(paths, request); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.changed); EXPECT_EQ(result.payload_id, newer_id); EXPECT_EQ(CurrentId(), newer_id); EXPECT_EQ(spawns, 2); } TEST_P(UpdateCoordinatorTest, NonEagainSpawnErrorKeepsExistingRejectionPolicy) { request.canary_spawn = Spawn(ENOEXEC); const auto first = RunUpdate(paths, request); ASSERT_TRUE(first) << first.error; EXPECT_FALSE(first.changed); EXPECT_EQ(first.payload_id, active_id); EXPECT_EQ(spawns, 1); EXPECT_TRUE(std::filesystem::exists(Marker())); request.canary_spawn = Spawn(); const auto second = RunUpdate(paths, request); ASSERT_TRUE(second) << second.error; EXPECT_FALSE(second.changed); EXPECT_EQ(CurrentId(), active_id); EXPECT_EQ(spawns, 1); } TEST_P(UpdateCoordinatorTest, CanaryReportsSpawnEagainAndCleansItsWorkspace) { auto options = Options(); options.spawn = Spawn(EAGAIN); const auto result = RunReadinessCanary(options); EXPECT_FALSE(result); EXPECT_EQ(result.spawn_error, EAGAIN); EXPECT_EQ(result.exit_code, -1); EXPECT_FALSE(result.error.empty()); EXPECT_EQ(spawns, 1); EXPECT_TRUE(std::filesystem::is_regular_file(result.log_path)); EXPECT_EQ(CurrentId(), active_id); for (const auto& entry : std::filesystem::directory_iterator(paths.cache_root)) { EXPECT_NE(entry.path().filename().string().find(".native-update-canary"), 0U); } EXPECT_FALSE(std::filesystem::exists(Marker())); } TEST_P(UpdateCoordinatorTest, CanaryFailureBeforeSpawnHasNoSpawnError) { auto options = Options(); options.spawn = Spawn(EAGAIN); options.timeout_seconds = 0; const auto result = RunReadinessCanary(options); EXPECT_FALSE(result); EXPECT_EQ(result.spawn_error, 0); EXPECT_EQ(result.exit_code, -1); EXPECT_FALSE(result.error.empty()); EXPECT_EQ(spawns, 0); } INSTANTIATE_TEST_SUITE_P(Startup, UpdateCoordinatorTest, ::testing::Values(false)); TEST(PayloadStoreTest, CollectsSupersededPayloadsAndKeepsTheRollbackTarget) { TemporaryDirectory temporary; const std::filesystem::path root = temporary.root() / "store"; const std::string active = "2998-" + std::string(40, 'a'); const std::string rollback = "2908-" + std::string(40, 'b'); const std::string reference = "2628-" + std::string(40, 'c'); const std::string superseded = "2546-" + std::string(40, 'd'); const std::string abandoned = ".stage-" + superseded + "-4242"; for (const std::string& id : {active, rollback, reference, superseded, abandoned}) { Write(root / "payloads" / id / "libroblox.so", "payload bytes"); } // Payloads are published read-only; the collector has to cope. ASSERT_EQ(chmod((root / "payloads" / superseded).c_str(), 0555), 0); Write(root / "current.json", ActivationManifestFixture(active)); Write(root / "previous_good.json", ActivationManifestFixture(rollback)); PayloadStore store(root, temporary.root() / "compatibility.json"); const PayloadGarbageResult collected = store.CollectGarbage({reference}); ASSERT_TRUE(collected) << collected.error; EXPECT_EQ(collected.removed.size(), 2U); EXPECT_GT(collected.freed_bytes, 0U); EXPECT_TRUE(std::filesystem::exists(root / "payloads" / active)); EXPECT_TRUE(std::filesystem::exists(root / "payloads" / rollback)); EXPECT_TRUE(std::filesystem::exists(root / "payloads" / reference)); EXPECT_FALSE(std::filesystem::exists(root / "payloads" / superseded)); EXPECT_FALSE(std::filesystem::exists(root / "payloads" / abandoned)); } // #110: the metadata carries the moment of preparation, so restaging the same // bytes quarantined the running payload, took it out of payloads/, and left // the next launch to download it again. TEST(PayloadStoreTest, RestagingIdenticalBytesKeepsTheInstalledPayload) { TemporaryDirectory temporary; const std::filesystem::path prepared = temporary.root() / "prepared"; 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" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; const std::string asset_hash = HashAssetTree(prepared / "assets", &asset_count, &error); ASSERT_TRUE(error.empty()) << error; nlohmann::json metadata = { {"schema_version", 1}, {"package", "com.roblox.client"}, {"version_name", "2.727.1199"}, {"version_code", 2628}, {"elf_build_id", "1686400865ae0e408cd7bd67de7a439625c6fd13"}, {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.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"}, }; Write(prepared / "roblox_payload.json", metadata.dump(2) + "\n"); std::filesystem::copy(prepared, restaged, std::filesystem::copy_options::recursive); metadata["imported_at"] = "2026-09-04T04:12:55Z"; Write(restaged / "roblox_payload.json", metadata.dump(2) + "\n"); const std::filesystem::path store_root = temporary.root() / "store"; PayloadStore store(store_root, temporary.root() / "compatibility.json"); const PayloadStoreResult initial = store.Stage(prepared); ASSERT_TRUE(initial) << initial.error; const auto written_at = std::filesystem::last_write_time(initial.payload_directory); const PayloadStoreResult again = store.Stage(restaged); ASSERT_TRUE(again) << again.error; EXPECT_EQ(again.payload_id, initial.payload_id); EXPECT_EQ(std::filesystem::last_write_time(initial.payload_directory), written_at); EXPECT_FALSE(std::filesystem::exists(store_root / "quarantine")); } TEST(PayloadStoreTest, CollectsNothingWhileTheStoreIsUnreadable) { TemporaryDirectory temporary; const std::filesystem::path root = temporary.root() / "store"; const std::string superseded = "2546-" + std::string(40, 'd'); Write(root / "payloads" / superseded / "libroblox.so", "payload bytes"); Write(root / "current.json", "{not json\n"); PayloadStore store(root, temporary.root() / "compatibility.json"); const PayloadGarbageResult collected = store.CollectGarbage({}); EXPECT_FALSE(collected); EXPECT_TRUE(std::filesystem::exists(root / "payloads" / superseded)); } TEST(PayloadStoreTest, StagesAndPromotesVerifiedExactPayload) { TemporaryDirectory temporary; 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" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; const std::string asset_hash = HashAssetTree(prepared / "assets", &asset_count, &error); ASSERT_TRUE(error.empty()) << error; constexpr std::string_view kBuildId = "1686400865ae0e408cd7bd67de7a439625c6fd13"; nlohmann::json metadata = { {"schema_version", 1}, {"package", "com.roblox.client"}, {"version_name", "2.727.1199"}, {"version_code", 2628}, {"elf_build_id", kBuildId}, {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.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"); const std::filesystem::path compatibility = temporary.root() / "compatibility.json"; Write(compatibility, "{\"schema_version\":1,\"profiles\":[{" "\"abi\":\"" + std::string(compat::kGuestAbi) + "\"," "\"version_name\":\"2.727.1199\",\"version_code\":2628," "\"elf_build_id\":\"1686400865ae0e408cd7bd67de7a439625c6fd13\"," "\"status\":\"supported\",\"default_allowed\":true," "\"allow_legacy_binary_patches\":false,\"abi\":\"" + std::string(compat::kGuestAbi) + "\"}]}\n"); PayloadStore store(temporary.root() / "store", compatibility); const PayloadStoreResult staged = store.Stage(prepared); ASSERT_TRUE(staged) << staged.error; const PayloadStoreResult promoted = store.Promote(staged.payload_id); ASSERT_TRUE(promoted) << promoted.error; const PayloadStoreResult current = store.VerifyCurrent(); ASSERT_TRUE(current) << current.error; EXPECT_EQ(current.payload_id, "2628-1686400865ae0e408cd7bd67de7a439625c6fd13"); const nlohmann::json activation = nlohmann::json::parse( ReadFile(temporary.root() / "store/current.json")); EXPECT_EQ(activation["version_name"], metadata["version_name"]); EXPECT_EQ(activation["version_code"], metadata["version_code"]); EXPECT_EQ(activation["elf_build_id"], metadata["elf_build_id"]); EXPECT_EQ(activation["payload_sha256"], metadata["sha256"]["libroblox"]); nlohmann::json tampered_activation = activation; tampered_activation["payload_sha256"] = std::string(64, '0'); Write(temporary.root() / "store/current.json", tampered_activation.dump(2) + "\n"); const PayloadStoreResult tampered = store.VerifyCurrent(); EXPECT_FALSE(tampered); EXPECT_EQ(tampered.error, "active payload manifest hash does not match payload"); } TEST(PayloadStoreTest, RestagesReadOnlyCorruptPayloadCollision) { TemporaryDirectory temporary; 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" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; const std::string asset_hash = HashAssetTree(prepared / "assets", &asset_count, &error); ASSERT_TRUE(error.empty()) << error; constexpr std::string_view kBuildId = "1686400865ae0e408cd7bd67de7a439625c6fd13"; const nlohmann::json metadata = { {"schema_version", 1}, {"package", "com.roblox.client"}, {"version_name", "2.727.1199"}, {"version_code", 2628}, {"elf_build_id", kBuildId}, {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.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"); const std::filesystem::path store_root = temporary.root() / "store"; PayloadStore store(store_root, temporary.root() / "compatibility.json"); const PayloadStoreResult initial = store.Stage(prepared); ASSERT_TRUE(initial) << initial.error; const std::filesystem::path stored_metadata = initial.payload_directory / "roblox_payload.json"; std::error_code filesystem_error; std::filesystem::permissions(stored_metadata, std::filesystem::perms::owner_write, std::filesystem::perm_options::add, filesystem_error); ASSERT_FALSE(filesystem_error); Write(stored_metadata, "corrupt\n"); std::filesystem::permissions(stored_metadata, std::filesystem::perms::owner_read, std::filesystem::perm_options::replace, filesystem_error); ASSERT_FALSE(filesystem_error); const PayloadStoreResult recovered = store.Stage(prepared); ASSERT_TRUE(recovered) << recovered.error; EXPECT_TRUE(VerifyPreparedPayload(recovered.payload_directory)); const std::filesystem::path quarantine = store_root / "quarantine"; std::filesystem::directory_iterator iterator(quarantine, filesystem_error); ASSERT_FALSE(filesystem_error); ASSERT_NE(iterator, std::filesystem::directory_iterator()); const auto quarantined_status = iterator->symlink_status(filesystem_error); ASSERT_FALSE(filesystem_error); EXPECT_EQ(quarantined_status.permissions() & std::filesystem::perms::owner_write, std::filesystem::perms::none); ++iterator; EXPECT_EQ(iterator, std::filesystem::directory_iterator()); } // Promotion is separated from staging by the readiness canaries, which run the // real client against the staged directory in a child process. A candidate // modified in that window must never become current, so promotion rehashes the // immutable bytes even though the very same instance staged them. TEST(PayloadStoreTest, PromoteRehashesBytesTamperedAfterStaging) { TemporaryDirectory temporary; 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" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; const std::string asset_hash = HashAssetTree(prepared / "assets", &asset_count, &error); ASSERT_TRUE(error.empty()) << error; const nlohmann::json metadata = { {"schema_version", 1}, {"package", "com.roblox.client"}, {"version_name", "2.727.1199"}, {"version_code", 2628}, {"elf_build_id", "1686400865ae0e408cd7bd67de7a439625c6fd13"}, {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.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"); const std::filesystem::path compatibility = temporary.root() / "compatibility.json"; Write(compatibility, "{\"schema_version\":1,\"profiles\":[{" "\"version_name\":\"2.727.1199\",\"version_code\":2628," "\"elf_build_id\":\"1686400865ae0e408cd7bd67de7a439625c6fd13\"," "\"status\":\"supported\",\"default_allowed\":true," "\"allow_legacy_binary_patches\":false,\"abi\":\"" + std::string(compat::kGuestAbi) + "\"}]}\n"); const std::filesystem::path store_root = temporary.root() / "store"; PayloadStore store(store_root, compatibility); const PayloadStoreResult staged = store.Stage(prepared); ASSERT_TRUE(staged) << staged.error; // Stands in for the canary interval. Content bytes only: the metadata still // describes the original payload, so nothing short of hashing the contents // can detect this. const std::filesystem::path asset = staged.payload_directory / "assets/content/fixture"; std::error_code filesystem_error; std::filesystem::permissions(asset, std::filesystem::perms::owner_write, std::filesystem::perm_options::add, filesystem_error); ASSERT_FALSE(filesystem_error); Write(asset, "tampered"); const PayloadStoreResult promoted = store.Promote(staged.payload_id); EXPECT_FALSE(promoted); EXPECT_EQ(promoted.error, "payload asset tree does not match metadata"); EXPECT_FALSE(std::filesystem::exists(store_root / "current.json", filesystem_error)); } // The same guarantee holds for a library modified in that window. TEST(PayloadStoreTest, PromoteRejectsALibraryTamperedAfterStaging) { TemporaryDirectory temporary; 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" / compat::kGuestSplitApkFile, "split-apk"); Write(prepared / "assets/content/fixture", "asset"); std::string error; std::size_t asset_count = 0; const std::string asset_hash = HashAssetTree(prepared / "assets", &asset_count, &error); ASSERT_TRUE(error.empty()) << error; const nlohmann::json metadata = { {"schema_version", 1}, {"package", "com.roblox.client"}, {"version_name", "2.727.1199"}, {"version_code", 2628}, {"elf_build_id", "1686400865ae0e408cd7bd67de7a439625c6fd13"}, {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.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"); const std::filesystem::path compatibility = temporary.root() / "compatibility.json"; Write(compatibility, "{\"schema_version\":1,\"profiles\":[{" "\"version_name\":\"2.727.1199\",\"version_code\":2628," "\"elf_build_id\":\"1686400865ae0e408cd7bd67de7a439625c6fd13\"," "\"status\":\"supported\",\"default_allowed\":true," "\"allow_legacy_binary_patches\":false,\"abi\":\"" + std::string(compat::kGuestAbi) + "\"}]}\n"); const std::filesystem::path store_root = temporary.root() / "store"; PayloadStore store(store_root, compatibility); const PayloadStoreResult staged = store.Stage(prepared); ASSERT_TRUE(staged) << staged.error; const std::filesystem::path library = staged.payload_directory / "libroblox.so"; std::error_code filesystem_error; std::filesystem::permissions(library, std::filesystem::perms::owner_write, std::filesystem::perm_options::add, filesystem_error); ASSERT_FALSE(filesystem_error); Write(library, "backdoored"); const PayloadStoreResult promoted = store.Promote(staged.payload_id); EXPECT_FALSE(promoted); EXPECT_EQ(promoted.error, "payload hash mismatch: libroblox.so"); EXPECT_FALSE(std::filesystem::exists(store_root / "current.json", filesystem_error)); } TEST(PayloadStoreTest, EmptyResultCannotReportFalsePromotion) { const PayloadStoreResult result; EXPECT_FALSE(result); } TEST(PayloadStoreTest, RequiresTwoRuntimeBoundCanariesForLatestCandidate) { TemporaryDirectory temporary; 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" / compat::kGuestSplitApkFile, "new-split-apk"); Write(prepared / "assets/content/fixture", "new-asset"); std::string error; std::size_t asset_count = 0; const std::string asset_hash = HashAssetTree(prepared / "assets", &asset_count, &error); ASSERT_TRUE(error.empty()) << error; constexpr std::string_view kBuildId = "a6c1f5c57f9d7aa3fb99a5fa30565e07e5c88f6a"; const nlohmann::json metadata = { {"schema_version", 1}, {"package", "com.roblox.client"}, {"version_name", "2.732.1043"}, {"version_code", 2814}, {"abi", std::string(compat::kGuestAbi)}, {"elf_build_id", kBuildId}, {"sha256", {{"libroblox", HashRegularFile(prepared / "libroblox.so")}, {"base_apk", HashRegularFile(prepared / "sober_apk/base.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"); const std::filesystem::path compatibility = temporary.root() / "repository-compatibility.json"; Write(compatibility, "{\"schema_version\":1,\"profiles\":[{" "\"version_name\":\"2.727.1199\",\"version_code\":2628," "\"elf_build_id\":\"1686400865ae0e408cd7bd67de7a439625c6fd13\"," "\"status\":\"supported\",\"default_allowed\":true," "\"allow_legacy_binary_patches\":false,\"abi\":\"" + std::string(compat::kGuestAbi) + "\"}]}\n"); std::error_code filesystem_error; const std::filesystem::path runtime = std::filesystem::canonical("/proc/self/exe", filesystem_error); ASSERT_FALSE(filesystem_error); PayloadStore store(temporary.root() / "store", compatibility, runtime); const PayloadStoreResult staged = store.Stage(prepared); ASSERT_TRUE(staged) << staged.error; const std::string payload_id = "2814-a6c1f5c57f9d7aa3fb99a5fa30565e07e5c88f6a"; const std::filesystem::path profile = temporary.root() / "candidate.json"; Write(profile, nlohmann::json( {{"schema_version", 1}, {"elf_build_id", kBuildId}, {"payload_sha256", metadata["sha256"]["libroblox"]}, {"payload_id", payload_id}, {"payload_path", "payloads/" + payload_id}, {"reference", {{"elf_build_id", "1686400865ae0e408cd7bd67de7a439625c6fd13"}, {"payload_sha256", "3e9c26c81186f93458ff65d8a8bc240c220974db02b8" "388b91340b77b336997d"}}}, {"profile", {{"elf_build_id", kBuildId}}}, {"derivation_anchors", {{"signature_version", 1}}}}) .dump(2) + "\n"); const std::filesystem::path candidate_compatibility = temporary.root() / "candidate-compatibility.json"; Write(candidate_compatibility, nlohmann::json( {{"schema_version", 1}, {"profiles", nlohmann::json::array( {{{"version_name", "2.732.1043"}, {"version_code", 2814}, {"elf_build_id", kBuildId}, {"status", "experimental"}, {"default_allowed", true}, {"allow_legacy_binary_patches", false}, {"allow_host_abi_bridges", true}, {"allow_host_constructor_replay", true}}})}}) .dump(2) + "\n"); const std::array canary_logs = { temporary.root() / "canary-1.log", temporary.root() / "canary-2.log", }; Write(canary_logs[0], "first independent Tier C pass\n"); Write(canary_logs[1], "second independent Tier C pass\n"); const PayloadStoreResult promoted = store.PromoteProbation( staged.payload_id, profile, candidate_compatibility, canary_logs); ASSERT_TRUE(promoted) << promoted.error; EXPECT_FALSE(promoted.approval_receipt.empty()); const PayloadStoreResult current = store.VerifyCurrent(); ASSERT_TRUE(current) << current.error; EXPECT_EQ(current.payload_id, payload_id); EXPECT_EQ(current.approval_receipt, promoted.approval_receipt); // A Mocktail rebuild does not invalidate a canary result for unchanged // Roblox bytes. The immutable receipt still binds the two original canary // runs, payload, profile, and compatibility manifest to one evidence set. nlohmann::json legacy_activation = nlohmann::json::parse( ReadFile(temporary.root() / "store/current.json")); legacy_activation.erase("payload_sha256"); Write(temporary.root() / "store/current.json", legacy_activation.dump(2) + "\n"); PayloadStore rebuilt_runtime_store(temporary.root() / "store", compatibility); const PayloadStoreResult cached = rebuilt_runtime_store.VerifyCurrent(); ASSERT_TRUE(cached) << cached.error; EXPECT_EQ(cached.payload_id, payload_id); EXPECT_EQ(cached.approval_receipt, promoted.approval_receipt); const nlohmann::json activation = nlohmann::json::parse( ReadFile(temporary.root() / "store/current.json")); EXPECT_EQ(activation["payload_sha256"], metadata["sha256"]["libroblox"]); } } // namespace } // namespace mocktail::update