aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
diff options
context:
space:
mode:
authorAlex Barney <thealexbarney@gmail.com>2021-07-13 01:19:28 -0700
committerGitHub <noreply@github.com>2021-07-13 01:19:28 -0700
commit19afb3209c48db5f8e4b5f48f0faee925cd20d9f (patch)
tree161c871efb10554403651fe5b0c287369dc754bf /Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
parent04dce402ac94679c5439038be1c8ce090e7ad4cb (diff)
Update to LibHac 0.13.1 (#2328)
Update the LibHac dependency to version 0.13.1. This brings a ton of improvements and changes such as: - Refactor `FsSrv` to match the official refactoring done in FS. - Change how the `Horizon` and `HorizonClient` classes are handled. Each client created represents a different process with its own process ID and client state. - Add FS access control to handle permissions for FS service method calls. - Add FS program registry to keep track of the program ID, location and permissions of each process. - Add FS program index map info manager to track the program IDs and indexes of multi-application programs. - Add all FS IPC interfaces. - Rewrite `Fs.Fsa` code to be more accurate. - Rewrite a lot of `FsSrv` code to be more accurate. - Extend directory save data to store `SaveDataExtraData` - Extend directory save data to lock the save directory to allow only one accessor at a time. - Improve waiting and retrying when encountering access issues in `LocalFileSystem` and `DirectorySaveDataFileSystem`. - More `IFileSystemProxy` methods should work now. - Probably a bunch more stuff. On the Ryujinx side: - Forward most `IFileSystemProxy` methods to LibHac. - Register programs and program index map info when launching an application. - Remove hacks and workarounds for missing LibHac functionality. - Recreate missing save data extra data found on emulator startup. - Create system save data that wasn't indexed correctly on an older LibHac version. `FsSrv` now enforces access control for each process. When a process tries to open a save data file system, FS reads the save's extra data to determine who the save owner is and if the caller has permission to open the save data. Previously-created save data did not have extra data created when the save was created. With access control checks in place, this means that processes with no permissions (most games) wouldn't be able to access their own save data. The extra data can be partially created from data in the save data indexer, which should be enough for access control purposes.
Diffstat (limited to 'Ryujinx.HLE/FileSystem/VirtualFileSystem.cs')
-rw-r--r--Ryujinx.HLE/FileSystem/VirtualFileSystem.cs345
1 files changed, 291 insertions, 54 deletions
diff --git a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
index ff3232c2..fbedd8d4 100644
--- a/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
+++ b/Ryujinx.HLE/FileSystem/VirtualFileSystem.cs
@@ -1,15 +1,23 @@
using LibHac;
using LibHac.Common;
+using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
+using LibHac.Fs.Shim;
using LibHac.FsSrv;
using LibHac.FsSystem;
+using LibHac.Ncm;
using LibHac.Spl;
using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem.Content;
using Ryujinx.HLE.HOS;
using System;
+using System.Buffers.Text;
+using System.Collections.Generic;
using System.IO;
+using System.Runtime.CompilerServices;
+using RightsId = LibHac.Fs.RightsId;
namespace Ryujinx.HLE.FileSystem
{
@@ -24,17 +32,15 @@ namespace Ryujinx.HLE.FileSystem
private static bool _isInitialized = false;
- public Keyset KeySet { get; private set; }
- public FileSystemServer FsServer { get; private set; }
- public FileSystemClient FsClient { get; private set; }
+ public KeySet KeySet { get; private set; }
public EmulatedGameCard GameCard { get; private set; }
public EmulatedSdCard SdCard { get; private set; }
- public ModLoader ModLoader {get; private set;}
+ public ModLoader ModLoader { get; private set; }
private VirtualFileSystem()
{
- Reload();
+ ReloadKeySet();
ModLoader = new ModLoader(); // Should only be created once
}
@@ -80,39 +86,6 @@ namespace Ryujinx.HLE.FileSystem
internal string GetSdCardPath() => MakeFullPath(SdCardPath);
public string GetNandPath() => MakeFullPath(NandPath);
- internal string GetSavePath(ServiceCtx context, SaveInfo saveInfo, bool isDirectory = true)
- {
- string saveUserPath = "";
- string baseSavePath = NandPath;
- ulong currentTitleId = saveInfo.TitleId;
-
- switch (saveInfo.SaveSpaceId)
- {
- case SaveSpaceId.NandUser: baseSavePath = UserNandPath; break;
- case SaveSpaceId.NandSystem: baseSavePath = SystemNandPath; break;
- case SaveSpaceId.SdCard: baseSavePath = Path.Combine(SdCardPath, "Nintendo"); break;
- }
-
- baseSavePath = Path.Combine(baseSavePath, "save");
-
- if (saveInfo.TitleId == 0 && saveInfo.SaveDataType == SaveDataType.SaveData)
- {
- currentTitleId = context.Process.TitleId;
- }
-
- if (saveInfo.SaveSpaceId == SaveSpaceId.NandUser)
- {
- saveUserPath = saveInfo.UserId.IsNull ? "savecommon" : saveInfo.UserId.ToString();
- }
-
- string savePath = Path.Combine(baseSavePath,
- saveInfo.SaveId.ToString("x16"),
- saveUserPath,
- saveInfo.SaveDataType == SaveDataType.SaveData ? currentTitleId.ToString("x16") : string.Empty);
-
- return MakeFullPath(savePath, isDirectory);
- }
-
public string GetFullPartitionPath(string partitionPath)
{
return MakeFullPath(partitionPath);
@@ -136,8 +109,8 @@ namespace Ryujinx.HLE.FileSystem
if (systemPath.StartsWith(baseSystemPath))
{
- string rawPath = systemPath.Replace(baseSystemPath, "");
- int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar);
+ string rawPath = systemPath.Replace(baseSystemPath, "");
+ int firstSeparatorOffset = rawPath.IndexOf(Path.DirectorySeparatorChar);
if (firstSeparatorOffset == -1)
{
@@ -196,33 +169,34 @@ namespace Ryujinx.HLE.FileSystem
return new DriveInfo(Path.GetPathRoot(GetBasePath()));
}
- public void Reload()
+ public void InitializeFsServer(LibHac.Horizon horizon, out HorizonClient fsServerClient)
{
- ReloadKeySet();
-
LocalFileSystem serverBaseFs = new LocalFileSystem(GetBasePath());
- DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet);
+ fsServerClient = horizon.CreatePrivilegedHorizonClient();
+ var fsServer = new FileSystemServer(fsServerClient);
+
+ DefaultFsServerObjects fsServerObjects = DefaultFsServerObjects.GetDefaultEmulatedCreators(serverBaseFs, KeySet, fsServer);
GameCard = fsServerObjects.GameCard;
- SdCard = fsServerObjects.SdCard;
+ SdCard = fsServerObjects.SdCard;
SdCard.SetSdCardInsertionStatus(true);
- FileSystemServerConfig fsServerConfig = new FileSystemServerConfig
+ var fsServerConfig = new FileSystemServerConfig
{
- FsCreators = fsServerObjects.FsCreators,
DeviceOperator = fsServerObjects.DeviceOperator,
- ExternalKeySet = KeySet.ExternalKeySet
+ ExternalKeySet = KeySet.ExternalKeySet,
+ FsCreators = fsServerObjects.FsCreators
};
- FsServer = new FileSystemServer(fsServerConfig);
- FsClient = FsServer.CreateFileSystemClient();
+ FileSystemServerInitializer.InitializeWithConfig(fsServerClient, fsServer, fsServerConfig);
}
-
- private void ReloadKeySet()
+ public void ReloadKeySet()
{
+ KeySet ??= KeySet.CreateDefaultKeySet();
+
string keyFile = null;
string titleKeyFile = null;
string consoleKeyFile = null;
@@ -256,7 +230,7 @@ namespace Ryujinx.HLE.FileSystem
}
}
- KeySet = ExternalKeyReader.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile);
+ ExternalKeyReader.ReadKeyFile(KeySet, keyFile, titleKeyFile, consoleKeyFile, null);
}
public void ImportTickets(IFileSystem fs)
@@ -277,6 +251,269 @@ namespace Ryujinx.HLE.FileSystem
}
}
+ // Save data created before we supported extra data in directory save data will not work properly if
+ // given empty extra data. Luckily some of that extra data can be created using the data from the
+ // save data indexer, which should be enough to check access permissions for user saves.
+ // Every single save data's extra data will be checked and fixed if needed each time the emulator is opened.
+ // Consider removing this at some point in the future when we don't need to worry about old saves.
+ public static Result FixExtraData(HorizonClient hos)
+ {
+ Result rc = GetSystemSaveList(hos, out List<ulong> systemSaveIds);
+ if (rc.IsFailure()) return rc;
+
+ rc = FixUnindexedSystemSaves(hos, systemSaveIds);
+ if (rc.IsFailure()) return rc;
+
+ rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.System);
+ if (rc.IsFailure()) return rc;
+
+ rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.User);
+ if (rc.IsFailure()) return rc;
+
+ rc = FixExtraDataInSpaceId(hos, SaveDataSpaceId.SdCache);
+ if (rc.IsFailure()) return rc;
+
+ return Result.Success;
+ }
+
+ private static Result FixExtraDataInSpaceId(HorizonClient hos, SaveDataSpaceId spaceId)
+ {
+ Span<SaveDataInfo> info = stackalloc SaveDataInfo[8];
+
+ Result rc = hos.Fs.OpenSaveDataIterator(out var iterator, spaceId);
+ if (rc.IsFailure()) return rc;
+
+ while (true)
+ {
+ rc = iterator.ReadSaveDataInfo(out long count, info);
+ if (rc.IsFailure()) return rc;
+
+ if (count == 0)
+ return Result.Success;
+
+ for (int i = 0; i < count; i++)
+ {
+ rc = FixExtraData(out bool wasFixNeeded, hos, in info[i]);
+
+ if (rc.IsFailure())
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Error {rc.ToStringWithName()} when fixing extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space");
+ }
+ else if (wasFixNeeded)
+ {
+ Logger.Info?.Print(LogClass.Application, $"Tried to rebuild extra data for save data 0x{info[i].SaveDataId:x} in the {spaceId} save data space");
+ }
+ }
+ }
+ }
+
+ // Gets a list of all the save data files or directories in the system partition.
+ private static Result GetSystemSaveList(HorizonClient hos, out List<ulong> list)
+ {
+ list = null;
+
+ var mountName = "system".ToU8Span();
+ DirectoryHandle handle = default;
+ List<ulong> localList = new List<ulong>();
+
+ try
+ {
+ Result rc = hos.Fs.MountBis(mountName, BisPartitionId.System);
+ if (rc.IsFailure()) return rc;
+
+ rc = hos.Fs.OpenDirectory(out handle, "system:/save".ToU8Span(), OpenDirectoryMode.All);
+ if (rc.IsFailure()) return rc;
+
+ DirectoryEntry entry = new DirectoryEntry();
+
+ while (true)
+ {
+ rc = hos.Fs.ReadDirectory(out long readCount, SpanHelpers.AsSpan(ref entry), handle);
+ if (rc.IsFailure()) return rc;
+
+ if (readCount == 0)
+ break;
+
+ if (Utf8Parser.TryParse(entry.Name, out ulong saveDataId, out int bytesRead, 'x') &&
+ bytesRead == 16 && (long)saveDataId < 0)
+ {
+ localList.Add(saveDataId);
+ }
+ }
+
+ list = localList;
+
+ return Result.Success;
+ }
+ finally
+ {
+ if (handle.IsValid)
+ {
+ hos.Fs.CloseDirectory(handle);
+ }
+
+ if (hos.Fs.IsMounted(mountName))
+ {
+ hos.Fs.Unmount(mountName);
+ }
+ }
+ }
+
+ // Adds system save data that isn't in the save data indexer to the indexer and creates extra data for it.
+ // Only save data IDs added to SystemExtraDataFixInfo will be fixed.
+ private static Result FixUnindexedSystemSaves(HorizonClient hos, List<ulong> existingSaveIds)
+ {
+ foreach (var fixInfo in SystemExtraDataFixInfo)
+ {
+ if (!existingSaveIds.Contains(fixInfo.StaticSaveDataId))
+ {
+ continue;
+ }
+
+ Result rc = FixSystemExtraData(out bool wasFixNeeded, hos, in fixInfo);
+
+ if (rc.IsFailure())
+ {
+ Logger.Warning?.Print(LogClass.Application,
+ $"Error {rc.ToStringWithName()} when fixing extra data for system save data 0x{fixInfo.StaticSaveDataId:x}");
+ }
+ else if (wasFixNeeded)
+ {
+ Logger.Info?.Print(LogClass.Application,
+ $"Tried to rebuild extra data for system save data 0x{fixInfo.StaticSaveDataId:x}");
+ }
+ }
+
+ return Result.Success;
+ }
+
+ private static Result FixSystemExtraData(out bool wasFixNeeded, HorizonClient hos, in ExtraDataFixInfo info)
+ {
+ wasFixNeeded = true;
+
+ Result rc = hos.Fs.Impl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData extraData, info.StaticSaveDataId);
+ if (!rc.IsSuccess())
+ {
+ if (!ResultFs.TargetNotFound.Includes(rc))
+ return rc;
+
+ // We'll reach this point only if the save data directory exists but it's not in the save data indexer.
+ // Creating the save will add it to the indexer while leaving its existing contents intact.
+ return hos.Fs.CreateSystemSaveData(info.StaticSaveDataId, UserId.InvalidId, info.OwnerId, info.DataSize,
+ info.JournalSize, info.Flags);
+ }
+
+ if (extraData.Attribute.StaticSaveDataId != 0 && extraData.OwnerId != 0)
+ {
+ wasFixNeeded = false;
+ return Result.Success;
+ }
+
+ extraData = new SaveDataExtraData
+ {
+ Attribute = { StaticSaveDataId = info.StaticSaveDataId },
+ OwnerId = info.OwnerId,
+ Flags = info.Flags,
+ DataSize = info.DataSize,
+ JournalSize = info.JournalSize
+ };
+
+ // Make a mask for writing the entire extra data
+ Unsafe.SkipInit(out SaveDataExtraData extraDataMask);
+ SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF);
+
+ return hos.Fs.Impl.WriteSaveDataFileSystemExtraData(SaveDataSpaceId.System, info.StaticSaveDataId,
+ in extraData, in extraDataMask);
+ }
+
+ private static Result FixExtraData(out bool wasFixNeeded, HorizonClient hos, in SaveDataInfo info)
+ {
+ wasFixNeeded = true;
+
+ Result rc = hos.Fs.Impl.ReadSaveDataFileSystemExtraData(out SaveDataExtraData extraData, info.SpaceId,
+ info.SaveDataId);
+ if (rc.IsFailure()) return rc;
+
+ // The extra data should have program ID or static save data ID set if it's valid.
+ // We only try to fix the extra data if the info from the save data indexer has a program ID or static save data ID.
+ bool canFixByProgramId = extraData.Attribute.ProgramId == ProgramId.InvalidId &&
+ info.ProgramId != ProgramId.InvalidId;
+
+ bool canFixBySaveDataId = extraData.Attribute.StaticSaveDataId == 0 && info.StaticSaveDataId != 0;
+
+ if (!canFixByProgramId && !canFixBySaveDataId)
+ {
+ wasFixNeeded = false;
+ return Result.Success;
+ }
+
+ // The save data attribute struct can be completely created from the save data info.
+ extraData.Attribute.ProgramId = info.ProgramId;
+ extraData.Attribute.UserId = info.UserId;
+ extraData.Attribute.StaticSaveDataId = info.StaticSaveDataId;
+ extraData.Attribute.Type = info.Type;
+ extraData.Attribute.Rank = info.Rank;
+ extraData.Attribute.Index = info.Index;
+
+ // The rest of the extra data can't be created from the save data info.
+ // On user saves the owner ID will almost certainly be the same as the program ID.
+ if (info.Type != LibHac.Fs.SaveDataType.System)
+ {
+ extraData.OwnerId = info.ProgramId.Value;
+ }
+ else
+ {
+ // Try to match the system save with one of the known saves
+ foreach (ExtraDataFixInfo fixInfo in SystemExtraDataFixInfo)
+ {
+ if (extraData.Attribute.StaticSaveDataId == fixInfo.StaticSaveDataId)
+ {
+ extraData.OwnerId = fixInfo.OwnerId;
+ extraData.Flags = fixInfo.Flags;
+ extraData.DataSize = fixInfo.DataSize;
+ extraData.JournalSize = fixInfo.JournalSize;
+
+ break;
+ }
+ }
+ }
+
+ // Make a mask for writing the entire extra data
+ Unsafe.SkipInit(out SaveDataExtraData extraDataMask);
+ SpanHelpers.AsByteSpan(ref extraDataMask).Fill(0xFF);
+
+ return hos.Fs.Impl.WriteSaveDataFileSystemExtraData(info.SpaceId, info.SaveDataId, in extraData, in extraDataMask);
+ }
+
+ struct ExtraDataFixInfo
+ {
+ public ulong StaticSaveDataId;
+ public ulong OwnerId;
+ public SaveDataFlags Flags;
+ public long DataSize;
+ public long JournalSize;
+ }
+
+ private static readonly ExtraDataFixInfo[] SystemExtraDataFixInfo =
+ {
+ new ExtraDataFixInfo()
+ {
+ StaticSaveDataId = 0x8000000000000030,
+ OwnerId = 0x010000000000001F,
+ Flags = SaveDataFlags.KeepAfterResettingSystemSaveDataWithoutUserSaveData,
+ DataSize = 0x10000,
+ JournalSize = 0x10000
+ },
+ new ExtraDataFixInfo()
+ {
+ StaticSaveDataId = 0x8000000000001040,
+ OwnerId = 0x0100000000001009,
+ Flags = SaveDataFlags.None,
+ DataSize = 0xC000,
+ JournalSize = 0xC000
+ }
+ };
+
public void Unload()
{
RomFs?.Dispose();
@@ -299,7 +536,7 @@ namespace Ryujinx.HLE.FileSystem
{
if (_isInitialized)
{
- throw new InvalidOperationException($"VirtualFileSystem can only be instantiated once!");
+ throw new InvalidOperationException("VirtualFileSystem can only be instantiated once!");
}
_isInitialized = true;