mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 12:00:49 +02:00
fix(update): derive the FMOD bridge from the shipped reference when the installed profile lacks it
This commit is contained in:
1 parent
4f45baf3f7
commit
a3b9c872a5
6 files changed
+214
-4
No files matched your search
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in new issue
Block a user