Let approved cemod Mods survive file updates via opt-in trust

Unsigned .cemod principals are keyed on the package SHA-256, so
replacing an approved Mod with a newer build changes its principal and
silently drops it back to unapproved on next launch. Add a per-Mod-ID
trust anchor (Options -> General settings -> CemuExtend) that, once
opted into, carries an existing grant forward to a new principal as
long as the update doesn't request additional permissions. Off by
default; hash-pinned re-approval remains the default behavior.
This commit is contained in:
kaoru767
2026-07-23 09:14:11 +09:00
parent 97bbe33360
commit 98839ec337
9 changed files with 119 additions and 10 deletions

View File

@@ -27,6 +27,8 @@ CEX2は`CEX2Query`、`CEX2Open`、`CEX2Submit`、`CEX2Poll`、`CEX2Cancel`、`CE
packageはユーザーデータの`cemuextend/mods/`へ配置します。`Options``General settings``CemuExtend`を開くと、manifestに含まれる対象ゲームが自動で一覧化されます。Mod名のチェックを付けると有効、外すと無効になり、変更は次回タイトル起動時に反映されます。Title IDの手入力は不要です。
Mod単位の権限パネルにある「Trust future updates to this Mod」はopt-inの例外です。有効にすると、その時点で承認した権限をMod ID単位で記録し、以後principalが変わっても要求権限が承認範囲を超えない限り再承認なしで自動的に引き継がれます。要求権限が増えた場合は従来どおり再承認が必要です。デフォルトは無効(未チェック)で、principalごとの再承認という既定の挙動は変わりません。
有効なModに未許可または新規要求の権限がある場合、CemuExtendはタイトルのマウント前に権限確認画面を表示します。複数Modは1画面にまとめられ、選択した権限を保存してからゲームを起動します。権限を一部拒否したまま起動することもできますが、不足が残る間は次回起動時にも確認されます。キャンセルした場合、ゲームは準備・起動されません。
### `isolated`

View File

@@ -13,4 +13,11 @@ namespace cemuextend_hle
return enabled && ((requested & ~granted) != 0 ||
(requested & ~approvedRequests) != 0);
}
[[nodiscard]] constexpr bool CemodTrustAnchorCoversRequest(std::uint32_t requested,
std::uint32_t anchorApprovedRequests)
{
requested &= kCemodPermissionMask;
return (requested & ~anchorApprovedRequests) == 0;
}
}

View File

@@ -243,6 +243,20 @@ namespace cemuextend_hle
return runtime;
}
CemuExtendModGrant ResolveCemodGrant(std::uint64_t titleId, const std::string& modId,
const std::string& principal, std::uint32_t requestedPermissions)
{
if (const auto exact = GetConfig().GetCemuExtendModGrant(titleId, principal))
return *exact;
const auto anchor = GetConfig().GetCemuExtendModTrustAnchor(titleId, modId);
if (!anchor || !CemodTrustAnchorCoversRequest(requestedPermissions, anchor->approved_request_mask))
return {};
const CemuExtendModGrant grant{anchor->permissions & requestedPermissions & kCemodPermissionMask,
anchor->approved_request_mask, true};
GetConfig().SetCemuExtendModGrant(titleId, principal, grant);
return grant;
}
std::vector<CemodPackageInfo> DiscoverCemodCatalog()
{
namespace fs = std::filesystem;
@@ -295,8 +309,8 @@ namespace cemuextend_hle
for (const auto& package : DiscoverCemods(titleId))
{
if (!package.error.empty()) continue;
const auto grant = GetConfig().GetCemuExtendModGrant(titleId, package.principal)
.value_or(CemuExtendModGrant{});
const auto grant = ResolveCemodGrant(titleId, package.modId, package.principal,
package.requestedPermissions & kCemodPermissionMask);
if (!grant.approved) continue;
auto found = grouped.try_emplace(package.principal,
CemodPermissionRequest{package.modId, package.principal, 0,
@@ -353,8 +367,8 @@ namespace cemuextend_hle
std::string error;
auto package = CemodPackage::Load(info.path, titleId, error);
if (!package) continue;
const auto grant = GetConfig().GetCemuExtendModGrant(titleId, package->principal)
.value_or(CemuExtendModGrant{});
const auto grant = ResolveCemodGrant(titleId, package->manifest.modId, package->principal,
package->manifest.requestedPermissions & kCemodPermissionMask);
if (!grant.approved || (package->manifest.requestedPermissions & ~grant.approved_request_mask) != 0)
{
cemuLog_log(LogType::Force,

View File

@@ -11,6 +11,7 @@
class ModExecutionContext;
class CemodRuntime;
struct CemuExtendModGrant;
struct CemodPackageInfo
{
@@ -39,6 +40,8 @@ namespace cemuextend_hle
COSModule* GetModule();
void ConfigureCex2HleAccess(ModExecutionContext& context);
CemodRuntime& GetCemodRuntime();
::CemuExtendModGrant ResolveCemodGrant(std::uint64_t titleId, const std::string& modId,
const std::string& principal, std::uint32_t requestedPermissions);
std::vector<CemodPackageInfo> DiscoverCemodCatalog();
std::vector<CemodPackageInfo> DiscoverCemods(std::uint64_t titleId);
std::vector<CemodPermissionRequest> PendingCemodPermissionRequests(std::uint64_t titleId);

View File

@@ -17,6 +17,7 @@ namespace
int main()
{
using cemuextend_hle::NeedsCemodPermissionPrompt;
using cemuextend_hle::CemodTrustAnchorCoversRequest;
CHECK(!NeedsCemodPermissionPrompt(0x1fU, 0, 0, false));
CHECK(!NeedsCemodPermissionPrompt(0, 0, 0, true));
@@ -24,5 +25,11 @@ int main()
CHECK(NeedsCemodPermissionPrompt(0x1fU, 0x0fU, 0x1fU, true));
CHECK(NeedsCemodPermissionPrompt(0x1fU, 0x1fU, 0x0fU, true));
CHECK(!NeedsCemodPermissionPrompt(0x20U, 0, 0, true));
CHECK(CemodTrustAnchorCoversRequest(0x0fU, 0x1fU));
CHECK(CemodTrustAnchorCoversRequest(0x1fU, 0x1fU));
CHECK(!CemodTrustAnchorCoversRequest(0x1fU, 0x0fU));
CHECK(!CemodTrustAnchorCoversRequest(0x02U, 0x01U));
CHECK(CemodTrustAnchorCoversRequest(0x20U, 0));
return 0;
}

View File

@@ -50,6 +50,33 @@ void CemuConfig::RemoveCemuExtendModGrant(uint64 titleId, std::string_view princ
if (title->second.empty()) cemuextend_mod_grants.erase(title);
}
std::optional<CemuExtendModTrustAnchor> CemuConfig::GetCemuExtendModTrustAnchor(uint64 titleId,
std::string_view modId) const
{
std::shared_lock lock(cemuextend_grants_mutex);
const auto title = cemuextend_mod_trust.find(titleId);
if (title == cemuextend_mod_trust.end()) return std::nullopt;
const auto mod = title->second.find(std::string(modId));
return mod == title->second.end() ? std::nullopt : std::optional{mod->second};
}
void CemuConfig::SetCemuExtendModTrustAnchor(uint64 titleId, std::string modId,
CemuExtendModTrustAnchor anchor)
{
if (titleId == 0 || modId.empty()) return;
std::unique_lock lock(cemuextend_grants_mutex);
cemuextend_mod_trust[titleId][std::move(modId)] = anchor;
}
void CemuConfig::RemoveCemuExtendModTrustAnchor(uint64 titleId, std::string_view modId)
{
std::unique_lock lock(cemuextend_grants_mutex);
const auto title = cemuextend_mod_trust.find(titleId);
if (title == cemuextend_mod_trust.end()) return;
title->second.erase(std::string(modId));
if (title->second.empty()) cemuextend_mod_trust.erase(title);
}
void CemuConfig::SetMLCPath(fs::path path, bool save)
{
mlc_path.SetValue(_pathToUtf8(path));
@@ -100,6 +127,7 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser)
std::unique_lock grantsLock(cemuextend_grants_mutex);
cemuextend_grants.clear();
cemuextend_mod_grants.clear();
cemuextend_mod_trust.clear();
auto bridge = parser.get("CemuExtend");
for (auto title = bridge.get("Title"); title.valid(); title = bridge.get("Title", title))
{
@@ -123,6 +151,16 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser)
mod.get_attribute<uint32>("approved_requests", 0) & 0x1fU,
mod.get_attribute<bool>("approved", false)};
}
for (auto trust = bridge.get("ModTrust"); trust.valid(); trust = bridge.get("ModTrust", trust))
{
const auto titleId = trust.get_attribute<uint64>("title", 0);
const std::string modId = trust.get_attribute("mod_id", "");
if (titleId == 0 || modId.empty() || modId.size() > 128)
continue;
cemuextend_mod_trust[titleId][modId] = {
trust.get_attribute<uint32>("permissions", 0) & 0x1fU,
trust.get_attribute<uint32>("approved_requests", 0) & 0x1fU};
}
}
std::unique_lock _lock(game_cache_entries_mutex);
@@ -412,6 +450,15 @@ XMLConfigParser CemuConfig::Save(XMLConfigParser& parser)
mod.set_attribute("approved_requests", grant.approved_request_mask & 0x1fU);
mod.set_attribute("approved", grant.approved);
}
for (const auto& [titleId, mods] : cemuextend_mod_trust)
for (const auto& [modId, anchor] : mods)
{
auto trust = bridge.set("ModTrust");
trust.set_attribute("title", static_cast<sint64>(titleId));
trust.set_attribute("mod_id", modId.c_str());
trust.set_attribute("permissions", anchor.permissions & 0x1fU);
trust.set_attribute("approved_requests", anchor.approved_request_mask & 0x1fU);
}
}
// game list cache

View File

@@ -79,6 +79,12 @@ struct CemuExtendModGrant
bool approved{};
};
struct CemuExtendModTrustAnchor
{
uint32 permissions{};
uint32 approved_request_mask{};
};
enum GraphicAPI
{
kOpenGL = 0,
@@ -530,10 +536,15 @@ struct CemuConfig
std::string_view principal) const;
void SetCemuExtendModGrant(uint64 titleId, std::string principal, CemuExtendModGrant grant);
void RemoveCemuExtendModGrant(uint64 titleId, std::string_view principal);
std::optional<CemuExtendModTrustAnchor> GetCemuExtendModTrustAnchor(uint64 titleId,
std::string_view modId) const;
void SetCemuExtendModTrustAnchor(uint64 titleId, std::string modId, CemuExtendModTrustAnchor anchor);
void RemoveCemuExtendModTrustAnchor(uint64 titleId, std::string_view modId);
mutable std::shared_mutex cemuextend_grants_mutex;
std::unordered_map<uint64, CemuExtendTitleGrant> cemuextend_grants;
std::unordered_map<uint64, std::unordered_map<std::string, CemuExtendModGrant>> cemuextend_mod_grants;
std::unordered_map<uint64, std::unordered_map<std::string, CemuExtendModTrustAnchor>> cemuextend_mod_trust;
// debug
ConfigValueBounds<CrashDump> crash_dump{ CrashDump::Disabled };

View File

@@ -1040,6 +1040,9 @@ wxPanel* GeneralSettings2::AddCemuExtendPage(wxNotebook* notebook)
wxString::FromUTF8(permissionNames[index]));
permissionBox->Add(m_cemod_permissions[index], 0, wxLEFT | wxRIGHT | wxBOTTOM, 6);
}
m_cemod_trust_updates = new wxCheckBox(permissionParent, wxID_ANY,
_("Trust future updates to this Mod (keep it enabled after the .cemod file is replaced with a newer version)"));
permissionBox->Add(m_cemod_trust_updates, 0, wxLEFT | wxRIGHT | wxBOTTOM, 6);
auto* modButtons = new wxBoxSizer(wxHORIZONTAL);
auto* import = new wxButton(permissionParent, wxID_ANY, _("Import legacy data…"));
auto* saveMod = new wxButton(permissionParent, wxID_ANY, _("Save permissions"));
@@ -1203,6 +1206,7 @@ void GeneralSettings2::RefreshCemodList()
if (!m_cemod_list) return;
m_cemod_list->Clear();
m_cemod_principals.clear();
m_cemod_mod_ids.clear();
m_cemod_requested.clear();
m_cemod_trusted.clear();
m_cemod_signed.clear();
@@ -1221,11 +1225,12 @@ void GeneralSettings2::RefreshCemodList()
package.executionMode == CemodExecutionMode::TrustedNative ? "trusted native" : "isolated",
package.signedPackage ? "signed" : "unsigned")));
m_cemod_principals.push_back(package.principal);
m_cemod_mod_ids.push_back(package.modId);
m_cemod_requested.push_back(package.requestedPermissions);
m_cemod_trusted.push_back(package.executionMode == CemodExecutionMode::TrustedNative);
m_cemod_signed.push_back(package.signedPackage);
const auto grant = GetConfig().GetCemuExtendModGrant(*titleId, package.principal)
.value_or(CemuExtendModGrant{});
const auto grant = cemuextend_hle::ResolveCemodGrant(*titleId, package.modId, package.principal,
package.requestedPermissions);
m_cemod_list->Check(m_cemod_principals.size() - 1,
grant.approved && (package.requestedPermissions & ~grant.approved_request_mask) == 0);
}
@@ -1243,8 +1248,8 @@ void GeneralSettings2::LoadCemodGrant()
const auto titleId = SelectedCemuExtendTitle();
const bool valid = selection != wxNOT_FOUND && static_cast<std::size_t>(selection) < m_cemod_principals.size() &&
titleId.has_value();
const auto grant = valid ? GetConfig().GetCemuExtendModGrant(*titleId,
m_cemod_principals[selection]).value_or(CemuExtendModGrant{}) : CemuExtendModGrant{};
const auto grant = valid ? cemuextend_hle::ResolveCemodGrant(*titleId, m_cemod_mod_ids[selection],
m_cemod_principals[selection], m_cemod_requested[selection]) : CemuExtendModGrant{};
const auto granted = grant.permissions;
const auto requested = valid ? m_cemod_requested[selection] : 0;
if (valid && m_cemod_trusted[selection])
@@ -1259,6 +1264,9 @@ void GeneralSettings2::LoadCemodGrant()
m_cemod_permissions[index]->Enable(valid && (requested & bit));
m_cemod_permissions[index]->SetValue(valid && (granted & bit));
}
m_cemod_trust_updates->Enable(valid);
m_cemod_trust_updates->SetValue(valid && GetConfig().GetCemuExtendModTrustAnchor(*titleId,
m_cemod_mod_ids[selection]).has_value());
}
void GeneralSettings2::ToggleCemod(std::size_t selection, bool enabled)
@@ -1276,16 +1284,19 @@ void GeneralSettings2::ToggleCemod(std::size_t selection, bool enabled)
return;
}
}
auto grant = GetConfig().GetCemuExtendModGrant(*titleId, m_cemod_principals[selection])
.value_or(CemuExtendModGrant{});
auto grant = cemuextend_hle::ResolveCemodGrant(*titleId, m_cemod_mod_ids[selection],
m_cemod_principals[selection], m_cemod_requested[selection]);
grant.permissions &= m_cemod_requested[selection];
grant.approved_request_mask = enabled ? m_cemod_requested[selection] : 0U;
grant.approved = enabled;
GetConfig().SetCemuExtendModGrant(*titleId, m_cemod_principals[selection], grant);
if (!enabled)
GetConfig().RemoveCemuExtendModTrustAnchor(*titleId, m_cemod_mod_ids[selection]);
GetConfigHandle().Save();
m_cemod_details->SetLabel(enabled ?
_("Enabled. The Mod will load on the next game launch.") :
_("Disabled. The Mod will not load on the next game launch."));
LoadCemodGrant();
}
void GeneralSettings2::StoreCemodGrant()
@@ -1302,6 +1313,11 @@ void GeneralSettings2::StoreCemodGrant()
const bool enabled = m_cemod_list->IsChecked(static_cast<unsigned int>(selection));
GetConfig().SetCemuExtendModGrant(*titleId, m_cemod_principals[selection],
{permissions, enabled ? m_cemod_requested[selection] : 0U, enabled});
if (enabled && m_cemod_trust_updates->IsChecked())
GetConfig().SetCemuExtendModTrustAnchor(*titleId, m_cemod_mod_ids[selection],
{permissions, m_cemod_requested[selection]});
else
GetConfig().RemoveCemuExtendModTrustAnchor(*titleId, m_cemod_mod_ids[selection]);
GetConfigHandle().Save();
m_cemod_details->SetLabel(_("Permissions saved. Changes apply on the next game launch."));
}

View File

@@ -110,7 +110,9 @@ private:
wxStaticText* m_cemod_status{};
wxStaticText* m_cemod_details{};
std::array<wxCheckBox*, 5> m_cemod_permissions{};
wxCheckBox* m_cemod_trust_updates{};
std::vector<std::string> m_cemod_principals;
std::vector<std::string> m_cemod_mod_ids;
std::vector<uint32> m_cemod_requested;
std::vector<bool> m_cemod_trusted;
std::vector<bool> m_cemod_signed;