fix(update): derive the FMOD bridge from the shipped reference when the installed profile lacks it

This commit is contained in:
vii committed 2026-10-10 00:33:00 -05:00
1 parent 4f45baf3f7
commit a3b9c872a5
6 files changed
+214 -4

No files matched your search

+4
View File
@@ -11,6 +11,10 @@ struct HostAbiDerivationOptions {
std::filesystem::path reference_library;
std::filesystem::path reference_profile;
std::vector<std::filesystem::path> reference_compatibility_manifests;
// Optional libroblox.so of another build that the compatibility manifests
// describe. It supplies the FMOD output-device anchors when the manifests
// have none for the reference build.
std::filesystem::path runtime_fallback_library;
std::filesystem::path candidate_payload_directory;
std::filesystem::path output_directory;
};
+47 -1
View File
@@ -23,6 +23,7 @@ supported.
from __future__ import annotations
import argparse
import contextlib
from dataclasses import dataclass
import hashlib
import json
@@ -1598,6 +1599,7 @@ def derive_runtime_compatibility(
reference: ElfImage,
candidate: ElfImage,
manifests: Sequence[Path],
fallback: ElfImage | None = None,
) -> dict[str, Any]:
source = load_reference_runtime_profile(manifests, reference.build_id)
result: dict[str, Any] = {}
@@ -1620,9 +1622,38 @@ def derive_runtime_compatibility(
result["fmod_output_device_bridge"] = derive_fmod_output_device_bridge(
reference, candidate, fmod
)
elif fallback is not None:
bridge = derive_fallback_fmod_output_device_bridge(
fallback, candidate, manifests
)
if bridge is not None:
result["fmod_output_device_bridge"] = bridge
return result
def derive_fallback_fmod_output_device_bridge(
fallback: ElfImage,
candidate: ElfImage,
manifests: Sequence[Path],
) -> dict[str, Any] | None:
"""Derive the optional FMOD bridge from another build in the manifests.
Any failure leaves the bridge out.
"""
if fallback.build_id == candidate.build_id:
return None
try:
fallback.validate_toolchain()
fmod = load_reference_runtime_profile(manifests, fallback.build_id).get(
"fmod_output_device_bridge"
)
if fmod is None:
return None
return derive_fmod_output_device_bridge(fallback, candidate, fmod)
except AnalyzerError:
return None
def find_registry_function_entry(
candidate: ElfImage,
body_rva: int,
@@ -2549,13 +2580,22 @@ def analyze(arguments: argparse.Namespace) -> tuple[dict[str, Any], dict[str, An
metadata_path = Path(arguments.payload_metadata)
sidecar_path = Path(arguments.reference_profile)
reference_sidecar = validated_sidecar(sidecar_path)
with ElfImage(reference_path) as reference, ElfImage(candidate_path) as candidate:
with contextlib.ExitStack() as stack:
reference = stack.enter_context(ElfImage(reference_path))
candidate = stack.enter_context(ElfImage(candidate_path))
fallback = None
if arguments.runtime_fallback_lib is not None:
with contextlib.suppress(AnalyzerError):
fallback = stack.enter_context(
ElfImage(Path(arguments.runtime_fallback_lib))
)
metadata = validated_payload_metadata(metadata_path, candidate_path, candidate)
profile, anchors = derive_profile(reference, candidate, reference_sidecar)
runtime_compatibility = derive_runtime_compatibility(
reference,
candidate,
tuple(Path(path) for path in arguments.reference_compatibility),
fallback,
)
return output_documents(
candidate,
@@ -2580,6 +2620,12 @@ def parse_arguments(argv: Sequence[str]) -> argparse.Namespace:
metavar="MANIFEST",
help="compatibility manifest containing exact reference runtime anchors",
)
parser.add_argument(
"--runtime-fallback-lib",
metavar="LIB",
help="libroblox.so of another build in the compatibility manifests; "
"supplies the FMOD output-device anchors when the reference has none",
)
parser.add_argument("--candidate-lib", required=True, metavar="LIB")
parser.add_argument("--payload-metadata", required=True, metavar="META")
parser.add_argument("--output", required=True, metavar="PROFILE")
+37 -3
View File
@@ -2563,6 +2563,35 @@ std::optional<std::pair<Json, Json>> DeriveDocuments(const ElfImage& reference,
return std::pair<Json, Json>{std::move(derived), std::move(compatibility)};
}
// Derives the optional FMOD output-device bridge from `library`, another build
// the manifests describe. Any failure leaves the manifest without the bridge.
void AddFallbackFmodBridge(const std::filesystem::path& library,
const std::vector<std::filesystem::path>& manifests,
const ElfImage& candidate, Json* compatibility) {
std::string error;
ElfImage fallback;
if (!fallback.Open(library, &error) ||
fallback.build_id() == candidate.build_id() ||
!fallback.ValidateToolchain(&error)) {
return;
}
const auto anchors =
LoadRuntimeCompatibilityAnchors(manifests, fallback.build_id(), &error);
if (!anchors.has_value() || !anchors->fmod.has_value()) return;
Disassembler disassembler;
if (!disassembler.Open(&error)) return;
RuntimeCompatibilityAnchors source;
source.fmod = anchors->fmod;
const auto derived = DeriveRuntimeCompatibility(fallback, candidate,
disassembler, source, &error);
if (!derived.has_value() ||
!derived->contains("fmod_output_device_bridge")) {
return;
}
(*compatibility)["profiles"][0]["fmod_output_device_bridge"] =
(*derived)["fmod_output_device_bridge"];
}
} // namespace
HostAbiSidecarIdentity ReadHostAbiSidecarIdentity(
@@ -2620,10 +2649,15 @@ HostAbiDerivationResult DeriveHostAbiProfile(
options.reference_compatibility_manifests, reference.build_id(),
&result.error);
if (!runtime_source.has_value()) return result;
const auto documents = DeriveDocuments(reference, candidate, *sidecar,
*metadata, *runtime_source,
&result.error);
auto documents = DeriveDocuments(reference, candidate, *sidecar, *metadata,
*runtime_source, &result.error);
if (!documents.has_value()) return result;
if (!runtime_source->fmod.has_value() &&
!options.runtime_fallback_library.empty()) {
AddFallbackFmodBridge(options.runtime_fallback_library,
options.reference_compatibility_manifests, candidate,
&documents->second);
}
result.profile = options.output_directory / "host_abi_profile.json";
result.compatibility_manifest =
options.output_directory / "compatibility.json";
+23
View File
@@ -301,6 +301,9 @@ struct ReferenceProfile {
std::filesystem::path library;
std::filesystem::path profile;
std::vector<std::filesystem::path> compatibility_manifests;
// libroblox.so of the shipped reference payload when the reference is a
// different, machine-derived payload. Empty when it is not staged.
std::filesystem::path runtime_fallback_library;
std::string error;
explicit operator bool() const {
@@ -367,6 +370,23 @@ ReferenceProfile ResolveReference(
paths.compatibility_manifest)) {
result.compatibility_manifests.push_back(paths.compatibility_manifest);
}
// A machine-derived profile can lack the optional FMOD output-device
// anchors. The shipped reference payload stays staged and has them.
const std::optional<SupportedPayloadProfile> shipped =
ResolveReferenceProfile(paths.host_abi_reference_profile, profiles,
nullptr);
if (shipped.has_value()) {
const std::string shipped_id = std::to_string(shipped->version_code) +
"-" + shipped->elf_build_id;
const std::filesystem::path shipped_directory =
paths.data_root / "payloads" / shipped_id;
const PayloadIntegrityResult shipped_payload =
InspectPreparedPayload(shipped_directory);
if (shipped_id != installed.payload_id && shipped_payload &&
shipped_payload.payload_id == shipped_id) {
result.runtime_fallback_library = shipped_directory / "libroblox.so";
}
}
return result;
}
}
@@ -475,6 +495,7 @@ UpdateResult RunUnsafeLatest(
derivation.reference_profile = reference.profile;
derivation.reference_compatibility_manifests =
reference.compatibility_manifests;
derivation.runtime_fallback_library = reference.runtime_fallback_library;
derivation.candidate_payload_directory = candidate.staged.payload_directory;
derivation.output_directory = workspace / "derived";
Progress(request.progress_fd, "Deriving latest Roblox compatibility...");
@@ -853,6 +874,8 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
derivation.reference_profile = reference.profile;
derivation.reference_compatibility_manifests =
reference.compatibility_manifests;
derivation.runtime_fallback_library =
reference.runtime_fallback_library;
derivation.candidate_payload_directory =
candidate.staged.payload_directory;
derivation.output_directory =
@@ -50,6 +50,10 @@ LATEST_CANDIDATE_METADATA = (
LATEST_REFERENCE_PROFILE = REFERENCE_PROFILE_PATH
DEVICE_LISTS_PAYLOAD_ID = "3092-5f0704edd9064f566ee3d6df2bd2fabbcc709f03"
DEVICE_LISTS_LIBRARY = PAYLOAD_ROOT / DEVICE_LISTS_PAYLOAD_ID / "libroblox.so"
FALLBACK_CANDIDATE_PAYLOAD_ID = "3222-a67a91ef96c700c0730acd36a250c2f93cfafbc1"
FALLBACK_CANDIDATE_LIBRARY = (
PAYLOAD_ROOT / FALLBACK_CANDIDATE_PAYLOAD_ID / "libroblox.so"
)
def load_analyzer():
@@ -799,5 +803,38 @@ class DeviceListsRuntimeCompatibilityAcceptanceTest(unittest.TestCase):
self.assertEqual(derived["fmod_output_device_bridge"], self.EXPECTED_BRIDGE)
@unittest.skipUnless(
ANALYZER.capstone is not None
and CANDIDATE_LIBRARY.is_file()
and DEVICE_LISTS_LIBRARY.is_file()
and FALLBACK_CANDIDATE_LIBRARY.is_file(),
"local exact 2998, 3092 and 3222 payloads are unavailable",
)
class FallbackRuntimeCompatibilityAcceptanceTest(unittest.TestCase):
MANIFESTS = (PROJECT_ROOT / "config" / "roblox_compatibility.json",)
def test_fallback_library_supplies_missing_fmod_bridge(self):
# The shipped manifest describes 2998 but not 3092.
with ANALYZER.ElfImage(DEVICE_LISTS_LIBRARY) as reference, ANALYZER.ElfImage(
FALLBACK_CANDIDATE_LIBRARY
) as candidate, ANALYZER.ElfImage(CANDIDATE_LIBRARY) as fallback:
without = ANALYZER.derive_runtime_compatibility(
reference, candidate, self.MANIFESTS)
derived = ANALYZER.derive_runtime_compatibility(
reference, candidate, self.MANIFESTS, fallback)
self.assertNotIn("fmod_output_device_bridge", without)
bridge = derived["fmod_output_device_bridge"]
self.assertEqual(bridge["vtable_layout_version"], 2)
self.assertIn("input_count_method_rva", bridge)
def test_fallback_without_anchors_leaves_bridge_out(self):
with ANALYZER.ElfImage(DEVICE_LISTS_LIBRARY) as reference, ANALYZER.ElfImage(
FALLBACK_CANDIDATE_LIBRARY
) as candidate:
derived = ANALYZER.derive_runtime_compatibility(
reference, candidate, self.MANIFESTS, reference)
self.assertNotIn("fmod_output_device_bridge", derived)
if __name__ == "__main__":
unittest.main()
+66
View File
@@ -2112,5 +2112,71 @@ TEST(PayloadStoreTest, RequiresTwoRuntimeBoundCanariesForLatestCandidate) {
metadata["sha256"]["libroblox"]);
}
// Needs real Roblox payloads. MOCKTAIL_TEST_UPDATE_DATA_ROOT is an updater data
// root whose derived 3092 profile has no FMOD output-device bridge.
TEST(HostAbiDeriverRealPayloadTest, FallbackLibrarySuppliesMissingFmodBridge) {
const char* data_root = std::getenv("MOCKTAIL_TEST_UPDATE_DATA_ROOT");
if (data_root == nullptr) {
GTEST_SKIP() << "real Roblox payloads were not explicitly provided";
}
const std::filesystem::path root = data_root;
const std::string reference_id =
"3092-5f0704edd9064f566ee3d6df2bd2fabbcc709f03";
// Derived profiles are named <payload id>-<runtime hash>.json.
std::string installed_profile;
std::error_code scan_error;
for (const auto& entry : std::filesystem::directory_iterator(
root / "host_abi_profiles", scan_error)) {
const std::string name = entry.path().filename().string();
if (name.rfind(reference_id + "-", 0) == 0 &&
std::filesystem::exists(root / "compatibility_profiles" / name)) {
installed_profile = name;
break;
}
}
ASSERT_FALSE(installed_profile.empty());
char pattern[] = "/tmp/mocktail_fmod_fallback_XXXXXX";
const char* created = mkdtemp(pattern);
ASSERT_NE(created, nullptr);
const std::filesystem::path output = created;
const auto derive = [&](const std::filesystem::path& directory,
const std::filesystem::path& fallback) {
HostAbiDerivationOptions options;
options.reference_library =
root / "payloads" / reference_id / "libroblox.so";
options.reference_profile = root / "host_abi_profiles" / installed_profile;
options.reference_compatibility_manifests = {
root / "compatibility_profiles" / installed_profile,
std::filesystem::path(MOCKTAIL_TEST_SOURCE_DIR) /
"config/roblox_compatibility.json"};
options.runtime_fallback_library = fallback;
options.candidate_payload_directory =
root / "payloads/3222-a67a91ef96c700c0730acd36a250c2f93cfafbc1";
options.output_directory = directory;
const HostAbiDerivationResult derived = DeriveHostAbiProfile(options);
EXPECT_TRUE(derived) << derived.error;
std::ifstream input(derived.compatibility_manifest);
const nlohmann::json manifest =
nlohmann::json::parse(input, nullptr, false);
return manifest.is_object() ? manifest["profiles"][0] : nlohmann::json();
};
const nlohmann::json without = derive(output / "without", {});
EXPECT_FALSE(without.contains("fmod_output_device_bridge"));
const nlohmann::json with = derive(
output / "with",
root / "payloads/2998-ade08266c67aee88ec9c1d00902150e1684dad3a/"
"libroblox.so");
ASSERT_TRUE(with.contains("fmod_output_device_bridge"));
EXPECT_TRUE(
with["fmod_output_device_bridge"].contains("input_count_method_rva"));
EXPECT_EQ(with["fmod_output_device_bridge"]["vtable_layout_version"], 2);
std::error_code filesystem_error;
std::filesystem::remove_all(output, filesystem_error);
}
} // namespace
} // namespace mocktail::update